1 /*
  2     Copyright 2008-2021
  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         },
201 
202         /**
203          * List of the element ids resp. values used as parents in {@link JXG.Board#create}.
204          * @returns {Array}
205          */
206         getParents: function () {
207             return Type.isArray(this.parents) ? this.parents : [];
208         },
209 
210         /**
211          * Update the cached coordinates of a group element.
212          * @param  {String} el element id of the group element whose cached coordinates
213          * are going to be updated.
214          * @return null
215          */
216         _updateCoordsCache: function(el) {
217             var obj;
218             if (el !== "" && Type.exists(this.objects[el])) {
219                 obj = this.objects[el].point;
220                 this.coords[obj.id] = {usrCoords: obj.coords.usrCoords.slice(0)};
221             }
222         },
223 
224         /**
225          * Sends an update to all group members.
226          * This method is called from the points' coords object event listeners
227          * and not by the board.
228          * @param{JXG.GeometryElement} drag Element that caused the update.
229          * @returns {JXG.Group} returns this group
230          */
231         update: function (drag) {
232             var 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