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