      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