1 /*
  2     Copyright 2008-2024
  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                     return this;
291                 }
292 
293                 if (drag.action === "rotation") {
294                     alpha = Geometry.rad(
295                         this.coords[drag.id].usrCoords.slice(1),
296                         center,
297                         this.objects[drag.id].point
298                     );
299                     t = this.board.create("transform", [alpha, center[0], center[1]], {
300                         type: "rotate"
301                     });
302                     t.update(); // This initializes t.matrix, which is needed if the action element is the first group element.
303                 } else if (drag.action === "scaling") {
304                     s = Geometry.distance(this.coords[drag.id].usrCoords.slice(1), center);
305                     if (Math.abs(s) < Mat.eps) {
306                         return this;
307                     }
308                     s = Geometry.distance(obj.coords.usrCoords.slice(1), center) / s;
309                     sx = this.scaleDirections[drag.id].indexOf("x") >= 0 ? s : 1.0;
310                     sy = this.scaleDirections[drag.id].indexOf("y") >= 0 ? s : 1.0;
311 
312                     // Shift scale center to origin, scale and shift the scale center back.
313                     t = this.board.create(
314                         "transform",
315                         [1, 0, 0, center[0] * (1 - sx), sx, 0, center[1] * (1 - sy), 0, sy],
316                         { type: "generic" }
317                     );
318                     t.update(); // This initializes t.matrix, which is needed if the action element is the first group element.
319                 } else {
320                     // This should not be reached
321                     return this;
322                 }
323             }
324 
325             // Bind the transformation to any images and texts
326             for (el in this.objects) {
327                 obj = this.objects[el].point;
328                 if (obj.elementClass !== Const.OBJECT_CLASS_POINT) {
329                     if (Type.exists(t.board)) {
330                         // t itself is a transformation
331                         t.meltTo(obj);
332                     } else {
333                         // Drag element is a point , therefore
334                         // t is an array and we have to use the transformation T.
335                         if (drag.id !== obj.id) {
336                             T.meltTo(obj);
337                         }
338                     }
339                 }
340             }
341 
342             this._update_apply_transformation(drag, t);
343 
344             this.needsUpdate = false; // This is needed here to prevent infinite recursion because
345             // of the board.updateElements call below,
346 
347             // Prepare dependent objects for update
348             for (el in this.objects) {
349                 if (this.objects.hasOwnProperty(el)) {
350                     for (desc in this.objects[el].descendants) {
351                         if (this.objects[el].descendants.hasOwnProperty(desc)) {
352                             this.objects[el].descendants.needsUpdate =
353                                 this.objects[el].descendants.needsRegularUpdate ||
354                                 this.board.needsFullUpdate;
355                         }
356                     }
357                 }
358             }
359             this.board.updateElements(drag);
360 
361             // Now, all group elements have their new position and
362             // we can update the bookkeeping of the coordinates of the group elements.
363             for (el in this.objects) {
364                 if (this.objects.hasOwnProperty(el)) {
365                     this._updateCoordsCache(el);
366                 }
367             }
368 
369             return this;
370         },
371 
372         /**
373          * @private
374         */
375         //  Determine what the dragging of a group element should do:
376         //  rotation, translation, scaling or nothing.
377         _update_find_drag_type: function () {
378             var el,
379                 obj,
380                 action = "nothing",
381                 changed = [],
382                 dragObjId;
383 
384             // Determine how many elements have changed their position
385             // If more than one element changed its position, it is a translation.
386             // If exactly one element changed its position we have to find the type of the point.
387             for (el in this.objects) {
388                 if (this.objects.hasOwnProperty(el)) {
389                     obj = this.objects[el].point;
390 
391                     if (obj.coords.distance(Const.COORDS_BY_USER, this.coords[el]) > Mat.eps) {
392                         changed.push(obj.id);
393                     }
394                 }
395             }
396 
397             // Determine type of action: translation, scaling or rotation
398             if (changed.length === 0) {
399                 return {
400                     action: action,
401                     id: "",
402                     changed: changed
403                 };
404             }
405 
406             dragObjId = changed[0];
407             obj = this.objects[dragObjId].point;
408 
409             if (changed.length > 1) {
410                 // More than one point moved => translation
411                 action = "translation";
412             } else {
413                 // One point moved => we have to determine the type
414                 if (
415                     Type.isInArray(this.rotationPoints, obj) &&
416                     Type.exists(this.rotationCenter)
417                 ) {
418                     action = "rotation";
419                 } else if (
420                     Type.isInArray(this.scalePoints, obj) &&
421                     Type.exists(this.scaleCenter)
422                 ) {
423                     action = "scaling";
424                 } else if (Type.isInArray(this.translationPoints, obj)) {
425                     action = "translation";
426                 }
427             }
428 
429             return {
430                 action: action,
431                 id: dragObjId,
432                 changed: changed
433             };
434         },
435 
436         /**
437          * @private
438          * @returns {Array} array of length two,
439         */
440         // Determine the Euclidean coordinates of the centroid of the group.
441         _update_centroid_center: function () {
442             var center, len, el;
443 
444             center = [0, 0];
445             len = 0;
446             for (el in this.coords) {
447                 if (this.coords.hasOwnProperty(el)) {
448                     center[0] += this.coords[el].usrCoords[1];
449                     center[1] += this.coords[el].usrCoords[2];
450                     ++len;
451                 }
452             }
453             if (len > 0) {
454                 center[0] /= len;
455                 center[1] /= len;
456             }
457 
458             return center;
459         },
460 
461         /**
462          * @private
463         */
464         // Apply the transformation to all elements of the group
465         _update_apply_transformation: function (drag, t) {
466             var el, obj;
467 
468             for (el in this.objects) {
469                 if (this.objects.hasOwnProperty(el)) {
470                     if (Type.exists(this.board.objects[el])) {
471                         obj = this.objects[el].point;
472 
473                         // Here, it is important that we change the position
474                         // of elements by using setCoordinates.
475                         // Thus, we avoid the call of snapToGrid().
476                         // This is done in the subsequent call of board.updateElements()
477                         // in Group.update() above.
478                         if (obj.id !== drag.id) {
479                             if (drag.action === "translation") {
480                                 if (!Type.isInArray(drag.changed, obj.id)) {
481                                     if (obj.elementClass === Const.OBJECT_CLASS_POINT) {
482                                         obj.coords.setCoordinates(Const.COORDS_BY_USER, [
483                                             this.coords[el].usrCoords[1] + t[0],
484                                             this.coords[el].usrCoords[2] + t[1]
485                                         ]);
486                                     }
487                                 }
488                             } else if (
489                                 drag.action === "rotation" ||
490                                 drag.action === "scaling"
491                             ) {
492                                 if (obj.elementClass === Const.OBJECT_CLASS_POINT) {
493                                     t.applyOnce([obj]);
494                                 }
495                             }
496                         } else {
497                             if (drag.action === "rotation" || drag.action === "scaling") {
498                                 obj.coords.setCoordinates(
499                                     Const.COORDS_BY_USER,
500                                     Mat.matVecMult(t.matrix, this.coords[obj.id].usrCoords)
501                                 );
502                             }
503                         }
504                     } else {
505                         delete this.objects[el];
506                     }
507                 }
508             }
509         },
510 
511         /**
512          * Adds an Point to this group.
513          * @param {JXG.Point} object The point added to the group.
514          * @returns {JXG.Group} returns this group
515          */
516         addPoint: function (object) {
517             this.objects[object.id] = { point: this.board.select(object) };
518             this._updateCoordsCache(object.id);
519             this.translationPoints.push(object);
520 
521             object.groups.push(this.id);
522             object.groups = Type.uniqueArray(object.groups);
523 
524             return this;
525         },
526 
527         /**
528          * Adds multiple points to this group.
529          * @param {Array} objects An array of points to add to the group.
530          * @returns {JXG.Group} returns this group
531          */
532         addPoints: function (objects) {
533             var p;
534 
535             for (p = 0; p < objects.length; p++) {
536                 this.addPoint(objects[p]);
537             }
538 
539             return this;
540         },
541 
542         /**
543          * Adds all points in a group to this group.
544          * @param {JXG.Group} group The group added to this group.
545          * @returns {JXG.Group} returns this group
546          */
547         addGroup: function (group) {
548             var el;
549 
550             for (el in group.objects) {
551                 if (group.objects.hasOwnProperty(el)) {
552                     this.addPoint(group.objects[el].point);
553                 }
554             }
555 
556             return this;
557         },
558 
559         /**
560          * Removes a point from the group.
561          * @param {JXG.Point} point
562          * @returns {JXG.Group} returns this group
563          */
564         removePoint: function (point) {
565             delete this.objects[point.id];
566 
567             return this;
568         },
569 
570         /**
571          * Sets the center of rotation for the group. This is either a point or the centroid of the group.
572          * @param {JXG.Point|String} object A point which will be the center of rotation, the string "centroid", or
573          * an array of length two, or a function returning an array of length two.
574          * @default 'centroid'
575          * @returns {JXG.Group} returns this group
576          */
577         setRotationCenter: function (object) {
578             this.rotationCenter = object;
579 
580             return this;
581         },
582 
583         /**
584          * Sets the rotation points of the group. Dragging at one of these points results into a rotation of the whole group around
585          * the rotation center of the group {@see JXG.Group#setRotationCenter}.
586          * @param {Array|JXG.Point} objects Array of {@link JXG.Point} or arbitrary number of {@link JXG.Point} elements.
587          * @returns {JXG.Group} returns this group
588          */
589         setRotationPoints: function (objects) {
590             return this._setActionPoints("rotation", objects);
591         },
592 
593         /**
594          * 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
595          * the rotation center of the group {@see JXG.Group#setRotationCenter}.
596          * @param {JXG.Point} point {@link JXG.Point} element.
597          * @returns {JXG.Group} returns this group
598          */
599         addRotationPoint: function (point) {
600             return this._addActionPoint("rotation", point);
601         },
602 
603         /**
604          * Removes the rotation property from a point of the group.
605          * @param {JXG.Point} point {@link JXG.Point} element.
606          * @returns {JXG.Group} returns this group
607          */
608         removeRotationPoint: function (point) {
609             return this._removeActionPoint("rotation", point);
610         },
611 
612         /**
613          * Sets the translation points of the group. Dragging at one of these points results into a translation of the whole group.
614          * @param {Array|JXG.Point} objects Array of {@link JXG.Point} or arbitrary number of {@link JXG.Point} elements.
615          *
616          * By default, all points of the group are translation points.
617          * @returns {JXG.Group} returns this group
618          */
619         setTranslationPoints: function (objects) {
620             return this._setActionPoints("translation", objects);
621         },
622 
623         /**
624          * Adds a point to the set of the translation points of the group.
625          * Dragging one of these points results into a translation of the whole group.
626          * @param {JXG.Point} point {@link JXG.Point} element.
627          * @returns {JXG.Group} returns this group
628          */
629         addTranslationPoint: function (point) {
630             return this._addActionPoint("translation", point);
631         },
632 
633         /**
634          * Removes the translation property from a point of the group.
635          * @param {JXG.Point} point {@link JXG.Point} element.
636          * @returns {JXG.Group} returns this group
637          */
638         removeTranslationPoint: function (point) {
639             return this._removeActionPoint("translation", point);
640         },
641 
642         /**
643          * Sets the center of scaling for the group. This is either a point or the centroid of the group.
644          * @param {JXG.Point|String} object A point which will be the center of scaling, the string "centroid", or
645          * an array of length two, or a function returning an array of length two.
646          * @returns {JXG.Group} returns this group
647          */
648         setScaleCenter: function (object) {
649             this.scaleCenter = object;
650 
651             return this;
652         },
653 
654         /**
655          * Sets the scale points of the group. Dragging at one of these points results into a scaling of the whole group.
656          * @param {Array|JXG.Point} objects Array of {@link JXG.Point} or arbitrary number of {@link JXG.Point} elements.
657          * @param {String} direction Restricts the directions to be scaled. Possible values are 'x', 'y', 'xy'. Default value is 'xy'.
658          *
659          * By default, all points of the group are translation points.
660          * @returns {JXG.Group} returns this group
661          */
662         setScalePoints: function (objects, direction) {
663             var objs, i, len;
664             if (Type.isArray(objects)) {
665                 objs = objects;
666             } else {
667                 objs = arguments;
668             }
669 
670             len = objs.length;
671             for (i = 0; i < len; ++i) {
672                 this.scaleDirections[this.board.select(objs[i]).id] = direction || "xy";
673             }
674 
675             return this._setActionPoints("scale", objects);
676         },
677 
678         /**
679          * 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.
680          * @param {JXG.Point} point {@link JXG.Point} element.
681          * @param {String} direction Restricts the directions to be scaled. Possible values are 'x', 'y', 'xy'. Default value is 'xy'.
682          * @returns {JXG.Group} returns this group
683          */
684         addScalePoint: function (point, direction) {
685             this._addActionPoint("scale", point);
686             this.scaleDirections[this.board.select(point).id] = direction || "xy";
687 
688             return this;
689         },
690 
691         /**
692          * Removes the scaling property from a point of the group.
693          * @param {JXG.Point} point {@link JXG.Point} element.
694          * @returns {JXG.Group} returns this group
695          */
696         removeScalePoint: function (point) {
697             return this._removeActionPoint("scale", point);
698         },
699 
700         /**
701          * Generic method for {@link JXG.Group@setTranslationPoints} and {@link JXG.Group@setRotationPoints}
702          * @private
703          */
704         _setActionPoints: function (action, objects) {
705             var objs, i, len;
706             if (Type.isArray(objects)) {
707                 objs = objects;
708             } else {
709                 objs = arguments;
710             }
711 
712             len = objs.length;
713             this[action + "Points"] = [];
714             for (i = 0; i < len; ++i) {
715                 this._addActionPoint(action, objs[i]);
716             }
717 
718             return this;
719         },
720 
721         /**
722          * Generic method for {@link JXG.Group@addTranslationPoint} and {@link JXG.Group@addRotationPoint}
723          * @private
724          */
725         _addActionPoint: function (action, point) {
726             this[action + "Points"].push(this.board.select(point));
727 
728             return this;
729         },
730 
731         /**
732          * Generic method for {@link JXG.Group@removeTranslationPoint} and {@link JXG.Group@removeRotationPoint}
733          * @private
734          */
735         _removeActionPoint: function (action, point) {
736             var idx = this[action + "Points"].indexOf(this.board.select(point));
737             if (idx > -1) {
738                 this[action + "Points"].splice(idx, 1);
739             }
740 
741             return this;
742         },
743 
744         /**
745          * @deprecated
746          * Use setAttribute
747          */
748         setProperty: function () {
749             JXG.deprecated("Group.setProperty", "Group.setAttribute()");
750             this.setAttribute.apply(this, arguments);
751         },
752 
753         setAttribute: function () {
754             var el;
755 
756             for (el in this.objects) {
757                 if (this.objects.hasOwnProperty(el)) {
758                     this.objects[el].point.setAttribute.apply(
759                         this.objects[el].point,
760                         arguments
761                     );
762                 }
763             }
764 
765             return this;
766         }
767     }
768 );
769 
770 /**
771  * @class This element combines a given set of {@link JXG.Point} elements to a
772  *  group. The elements of the group and dependent elements can be translated, rotated and scaled by
773  *  dragging one of the group elements.
774  *
775  *
776  * @pseudo
777  * @name Group
778  * @augments JXG.Group
779  * @constructor
780  * @type JXG.Group
781  * @param {JXG.Board} board The board the points are on.
782  * @param {Array} parents Array of points to group.
783  * @param {Object} attributes Visual properties (unused).
784  * @returns {JXG.Group}
785  *
786  * @example
787  *
788  *  // Create some free points. e.g. A, B, C, D
789  *  // Create a group
790  *
791  *  var p, col, g;
792  *  col = 'blue';
793  *  p = [];
794  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
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  *  g = board.create('group', p);
799  *
800  * </pre><div class="jxgbox" id="JXGa2204533-db91-4af9-b720-70394de4d367" style="width: 400px; height: 300px;"></div>
801  * <script type="text/javascript">
802  *  (function () {
803  *  var board, p, col, g;
804  *  board = JXG.JSXGraph.initBoard('JXGa2204533-db91-4af9-b720-70394de4d367', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
805  *  col = 'blue';
806  *  p = [];
807  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
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  *  g = board.create('group', p);
812  *  })();
813  * </script><pre>
814  *
815  *
816  * @example
817  *
818  *  // Create some free points. e.g. A, B, C, D
819  *  // Create a group
820  *  // If the points define a polygon and the polygon has the attribute hasInnerPoints:true,
821  *  // the polygon can be dragged around.
822  *
823  *  var p, col, pol, g;
824  *  col = 'blue';
825  *  p = [];
826  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
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  *
831  *  pol = board.create('polygon', p, {hasInnerPoints: true});
832  *  g = board.create('group', p);
833  *
834  * </pre><div class="jxgbox" id="JXG781b5564-a671-4327-81c6-de915c8f924e" style="width: 400px; height: 300px;"></div>
835  * <script type="text/javascript">
836  *  (function () {
837  *  var board, p, col, pol, g;
838  *  board = JXG.JSXGraph.initBoard('JXG781b5564-a671-4327-81c6-de915c8f924e', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
839  *  col = 'blue';
840  *  p = [];
841  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
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  *  pol = board.create('polygon', p, {hasInnerPoints: true});
846  *  g = board.create('group', p);
847  *  })();
848  * </script><pre>
849  *
850  *  @example
851  *
852  *  // Allow rotations:
853  *  // Define a center of rotation and declare points of the group as "rotation points".
854  *
855  *  var p, col, pol, g;
856  *  col = 'blue';
857  *  p = [];
858  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
859  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
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:col, fillColor:col}));
862  *
863  *  pol = board.create('polygon', p, {hasInnerPoints: true});
864  *  g = board.create('group', p);
865  *  g.setRotationCenter(p[0]);
866  *  g.setRotationPoints([p[1], p[2]]);
867  *
868  * </pre><div class="jxgbox" id="JXGf0491b62-b377-42cb-b55c-4ef5374b39fc" style="width: 400px; height: 300px;"></div>
869  * <script type="text/javascript">
870  *  (function () {
871  *  var board, p, col, pol, g;
872  *  board = JXG.JSXGraph.initBoard('JXGf0491b62-b377-42cb-b55c-4ef5374b39fc', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
873  *  col = 'blue';
874  *  p = [];
875  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
876  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
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:col, fillColor:col}));
879  *  pol = board.create('polygon', p, {hasInnerPoints: true});
880  *  g = board.create('group', p);
881  *  g.setRotationCenter(p[0]);
882  *  g.setRotationPoints([p[1], p[2]]);
883  *  })();
884  * </script><pre>
885  *
886  *  @example
887  *
888  *  // Allow rotations:
889  *  // As rotation center, arbitrary points, coordinate arrays,
890  *  // or functions returning coordinate arrays can be given.
891  *  // Another possibility is to use the predefined string 'centroid'.
892  *
893  *  // The methods to define the rotation points can be chained.
894  *
895  *  var p, col, pol, g;
896  *  col = 'blue';
897  *  p = [];
898  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
899  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
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:col, fillColor:col}));
902  *
903  *  pol = board.create('polygon', p, {hasInnerPoints: true});
904  *  g = board.create('group', p).setRotationCenter('centroid').setRotationPoints([p[1], p[2]]);
905  *
906  * </pre><div class="jxgbox" id="JXG8785b099-a75e-4769-bfd8-47dd4376fe27" style="width: 400px; height: 300px;"></div>
907  * <script type="text/javascript">
908  *  (function () {
909  *  var board, p, col, pol, g;
910  *  board = JXG.JSXGraph.initBoard('JXG8785b099-a75e-4769-bfd8-47dd4376fe27', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
911  *  col = 'blue';
912  *  p = [];
913  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
914  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
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:col, fillColor:col}));
917  *  pol = board.create('polygon', p, {hasInnerPoints: true});
918  *  g = board.create('group', p).setRotationCenter('centroid').setRotationPoints([p[1], p[2]]);
919  *  })();
920  * </script><pre>
921  *
922  *  @example
923  *
924  *  // Allow scaling:
925  *  // As for rotation one can declare points of the group to trigger a scaling operation.
926  *  // For this, one has to define a scaleCenter, in analogy to rotations.
927  *
928  *  // Here, the yellow  point enables scaling, the red point a rotation.
929  *
930  *  var p, col, pol, g;
931  *  col = 'blue';
932  *  p = [];
933  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
934  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'yellow', fillColor:'yellow'}));
935  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
936  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
937  *
938  *  pol = board.create('polygon', p, {hasInnerPoints: true});
939  *  g = board.create('group', p).setRotationCenter('centroid').setRotationPoints([p[2]]);
940  *  g.setScaleCenter(p[0]).setScalePoints(p[1]);
941  *
942  * </pre><div class="jxgbox" id="JXGc3ca436b-e4fc-4de5-bab4-09790140c675" style="width: 400px; height: 300px;"></div>
943  * <script type="text/javascript">
944  *  (function () {
945  *  var board, p, col, pol, g;
946  *  board = JXG.JSXGraph.initBoard('JXGc3ca436b-e4fc-4de5-bab4-09790140c675', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
947  *  col = 'blue';
948  *  p = [];
949  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
950  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'yellow', fillColor:'yellow'}));
951  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
952  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
953  *  pol = board.create('polygon', p, {hasInnerPoints: true});
954  *  g = board.create('group', p).setRotationCenter('centroid').setRotationPoints([p[2]]);
955  *  g.setScaleCenter(p[0]).setScalePoints(p[1]);
956  *  })();
957  * </script><pre>
958  *
959  *  @example
960  *
961  *  // Allow Translations:
962  *  // By default, every point of a group triggers a translation.
963  *  // There may be situations, when this is not wanted.
964  *
965  *  // In this example, E triggers nothing, but itself is rotation center
966  *  // and is translated, if other points are moved around.
967  *
968  *  var p, q, col, pol, g;
969  *  col = 'blue';
970  *  p = [];
971  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
972  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'yellow', fillColor:'yellow'}));
973  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
974  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
975  *  q = board.create('point',[0, 0], {size: 5, strokeColor:col, fillColor:col});
976  *
977  *  pol = board.create('polygon', p, {hasInnerPoints: true});
978  *  g = board.create('group', p.concat(q)).setRotationCenter('centroid').setRotationPoints([p[2]]);
979  *  g.setScaleCenter(p[0]).setScalePoints(p[1]);
980  *  g.removeTranslationPoint(q);
981  *
982  * </pre><div class="jxgbox" id="JXGd19b800a-57a9-4303-b49a-8f5b7a5488f0" style="width: 400px; height: 300px;"></div>
983  * <script type="text/javascript">
984  *  (function () {
985  *  var board, p, q, col, pol, g;
986  *  board = JXG.JSXGraph.initBoard('JXGd19b800a-57a9-4303-b49a-8f5b7a5488f0', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
987  *  col = 'blue';
988  *  p = [];
989  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
990  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'yellow', fillColor:'yellow'}));
991  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
992  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
993  *  q = board.create('point',[0, 0], {size: 5, strokeColor:col, fillColor:col});
994  *
995  *  pol = board.create('polygon', p, {hasInnerPoints: true});
996  *  g = board.create('group', p.concat(q)).setRotationCenter('centroid').setRotationPoints([p[2]]);
997  *  g.setScaleCenter(p[0]).setScalePoints(p[1]);
998  *  g.removeTranslationPoint(q);
999  *  })();
1000  * </script><pre>
1001  *
1002  *  @example
1003  *
1004  *        // Add an image and use the group tools to manipulate it
1005  *       let urlImg = "https://jsxgraph.org/distrib/images/uccellino.jpg";
1006  *       let lowleft = [-2, -1]
1007  *
1008  *       let col = 'blue';
1009  *       let p = [];
1010  *       p.push(board.create('point', lowleft, { size: 5, strokeColor: col, fillColor: col }));
1011  *       p.push(board.create('point', [2, -1], { size: 5, strokeColor: 'yellow', fillColor: 'yellow', name: 'scale' }));
1012  *       p.push(board.create('point', [2, 1], { size: 5, strokeColor: 'red', fillColor: 'red', name: 'rotate' }));
1013  *       p.push(board.create('point', [-2, 1], { size: 5, strokeColor: col, fillColor: col, name: 'translate' }));
1014  *
1015  *       let im = board.create('image', [urlImg, lowleft, [2, 2]]);
1016  *       let pol = board.create('polygon', p, { hasInnerPoints: true });
1017  *
1018  *       let g = board.create('group', p.concat(im))
1019  *       // g.addPoint(im)   // image, but adds as a point
1020  *
1021  *       g.setRotationCenter(lowleft)
1022  *       g.setRotationPoints([p[2]]);
1023  *
1024  *       g.setScaleCenter(p[0]).setScalePoints(p[1]);
1025  *
1026  * </pre><div class="jxgbox" id="JXGd19b800a-57a9-4303-b49a-8f5b7a5489f1" style="width: 400px; height: 300px;"></div>
1027  * <script type="text/javascript">
1028  *  (function () {
1029  *       let board = JXG.JSXGraph.initBoard('JXGd19b800a-57a9-4303-b49a-8f5b7a5489f1')
1030  *
1031  *       // Add an image and use the group tools to manipulate it
1032  *       let urlImg = "https://jsxgraph.org/distrib/images/uccellino.jpg";
1033  *       let lowleft = [-2, -1]
1034  *
1035  *       let col = 'blue';
1036  *       let p = [];
1037  *       p.push(board.create('point', lowleft, { size: 5, strokeColor: col, fillColor: col }));
1038  *       p.push(board.create('point', [2, -1], { size: 5, strokeColor: 'yellow', fillColor: 'yellow', name: 'scale' }));
1039  *       p.push(board.create('point', [2, 1], { size: 5, strokeColor: 'red', fillColor: 'red', name: 'rotate' }));
1040  *       p.push(board.create('point', [-2, 1], { size: 5, strokeColor: col, fillColor: col, name: 'translate' }));
1041  *
1042  *       let im = board.create('image', [urlImg, lowleft, [2, 2]]);
1043  *       let pol = board.create('polygon', p, { hasInnerPoints: true });
1044  *
1045  *       let g = board.create('group', p.concat(im))
1046  *       // g.addPoint(im)   // image, but adds as a point
1047  *
1048  *       g.setRotationCenter(lowleft)
1049  *       g.setRotationPoints([p[2]]);
1050  *
1051  *       g.setScaleCenter(p[0]).setScalePoints(p[1]);
1052  *  })();
1053  * </script><pre>
1054  */
1055 JXG.createGroup = function (board, parents, attributes) {
1056     var attr = Type.copyAttributes(attributes, board.options, "group"),
1057         g = new JXG.Group(board, attr.id, attr.name, parents, attr);
1058 
1059     g.elType = "group";
1060     g.setParents(parents);
1061 
1062     return g;
1063 };
1064 
1065 JXG.registerElement("group", JXG.createGroup);
1066 
1067 export default JXG.Group;
1068 // export default {
1069 //     Group: JXG.Group,
1070 //     createGroup: JXG.createGroup
1071 // };
1072