MDL-68454 mod_feedback: Update get_context to match parent
[moodle.git] / question / type / ddmarker / amd / src / shapes.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /* eslint max-depth: ["error", 8] */
18 /**
19  * Library of classes for handling simple shapes.
20  *
21  * These classes can represent shapes, let you alter them, can go to and from a string
22  * representation, and can give you an SVG representation.
23  *
24  * @package    qtype_ddmarker
25  * @subpackage shapes
26  * @copyright  2018 The Open University
27  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28  */
30 define(function() {
32     "use strict";
34     /**
35      * A point, with x and y coordinates.
36      *
37      * @param {int} x centre X.
38      * @param {int} y centre Y.
39      * @constructor
40      */
41     function Point(x, y) {
42         this.x = x;
43         this.y = y;
44     }
46     /**
47      * Standard toString method.
48      * @returns {string} "x;y";
49      */
50     Point.prototype.toString = function() {
51         return this.x + ',' + this.y;
52     };
54     /**
55      * Move a point
56      * @param {int} dx x offset
57      * @param {int} dy y offset
58      */
59     Point.prototype.move = function(dx, dy) {
60         this.x += dx;
61         this.y += dy;
62     };
64     /**
65      * Return a new point that is a certain position relative to this one.
66      *
67      * @param {(int|Point)} offsetX if a point, offset by this points coordinates, else and int x offset.
68      * @param {int} [offsetY] used if offsetX is an int, the corresponding y offset.
69      * @return {Point} the new point.
70      */
71     Point.prototype.offset = function(offsetX, offsetY) {
72         if (offsetX instanceof Point) {
73             offsetY = offsetX.y;
74             offsetX = offsetX.x;
75         }
76         return new Point(this.x + offsetX, this.y + offsetY);
77     };
79     /**
80      * Make a point from the string representation.
81      *
82      * @param {String} coordinates "x,y".
83      * @return {Point} the point. Throws an exception if input is not valid.
84      */
85     Point.parse = function(coordinates) {
86         var bits = coordinates.split(',');
87         if (bits.length !== 2) {
88             throw new Error(coordinates + ' is not a valid point');
89         }
90         return new Point(Math.round(bits[0]), Math.round(bits[1]));
91     };
94     /**
95      * Shape constructor. Abstract class to represent the different types of drop zone shapes.
96      *
97      * @param {String} [label] name of this area.
98      * @param {int} [x] centre X.
99      * @param {int} [y] centre Y.
100      * @constructor
101      */
102     function Shape(label, x, y) {
103         this.label = label;
104         this.centre = new Point(x || 0, y || 0);
105     }
107     /**
108      * Get the type of shape.
109      *
110      * @return {String} 'circle', 'rectangle' or 'polygon';
111      */
112     Shape.prototype.getType = function() {
113         throw new Error('Not implemented.');
114     };
116     /**
117      * Get the string representation of this shape.
118      *
119      * @return {String} coordinates as they need to be typed into the form.
120      */
121     Shape.prototype.getCoordinates = function() {
122         throw new Error('Not implemented.');
123     };
125     /**
126      * Update the shape from the string representation.
127      *
128      * @param {String} coordinates in the form returned by getCoordinates.
129      * @return {boolean} true if the string could be parsed and the shape updated, else false.
130      */
131     Shape.prototype.parse = function(coordinates) {
132         void (coordinates);
133         throw new Error('Not implemented.');
134     };
136     /**
137      * Move the entire shape by this offset.
138      *
139      * @param {int} dx x offset.
140      * @param {int} dy y offset.
141      * @param {int} maxX ensure that after editing, the shape lies between 0 and maxX on the x-axis.
142      * @param {int} maxY ensure that after editing, the shape lies between 0 and maxX on the y-axis.
143      */
144     Shape.prototype.move = function(dx, dy, maxX, maxY) {
145         void (maxY);
146     };
148     /**
149      * Move one of the edit handles by this offset.
150      *
151      * @param {int} handleIndex which handle was moved.
152      * @param {int} dx x offset.
153      * @param {int} dy y offset.
154      * @param {int} maxX ensure that after editing, the shape lies between 0 and maxX on the x-axis.
155      * @param {int} maxY ensure that after editing, the shape lies between 0 and maxX on the y-axis.
156      */
157     Shape.prototype.edit = function(handleIndex, dx, dy, maxX, maxY) {
158         void (maxY);
159     };
161     /**
162      * Update the properties of this shape after a sequence of edits.
163      *
164      * For example make sure the circle radius is positive, of the polygon centre is centred.
165      */
166     Shape.prototype.normalizeShape = function() {
167         void (1); // To make CiBoT happy.
168     };
170     /**
171      * Get the string representation of this shape.
172      *
173      * @param {SVGElement} svg the SVG graphic to add this shape to.
174      * @return {SVGElement} SVG representation of this shape.
175      */
176     Shape.prototype.makeSvg = function(svg) {
177         void (svg);
178         throw new Error('Not implemented.');
179     };
181     /**
182      * Update the SVG representation of this shape.
183      *
184      * @param {SVGElement} svgEl the SVG representation of this shape.
185      */
186     Shape.prototype.updateSvg = function(svgEl) {
187         void (svgEl);
188     };
190     /**
191      * Make a circle similar to this shape.
192      *
193      * @return {Circle} a circle that is about the same size and position as this shape.
194      */
195     Shape.prototype.makeSimilarCircle = function() {
196         throw new Error('Not implemented.');
197     };
199     /**
200      * Make a rectangle similar to this shape.
201      *
202      * @return {Rectangle} a rectangle that is about the same size and position as this shape.
203      */
204     Shape.prototype.makeSimilarRectangle = function() {
205         throw new Error('Not implemented.');
206     };
208     /**
209      * Make a polygon similar to this shape.
210      *
211      * @return {Polygon} a polygon that is about the same size and position as this shape.
212      */
213     Shape.prototype.makeSimilarPolygon = function() {
214         throw new Error('Not implemented.');
215     };
217     /**
218      * Get the handles that should be offered to edit this shape, or null if not appropriate.
219      *
220      * @return {[Object]} with properties moveHandle {Point} and editHandles {Point[]}
221      */
222     Shape.prototype.getHandlePositions = function() {
223         return null;
224     };
227     /**
228      * A shape that is a circle.
229      *
230      * @param {String} label name of this area.
231      * @param {int} [x] centre X.
232      * @param {int} [y] centre Y.
233      * @param {int} [radius] radius.
234      * @constructor
235      */
236     function Circle(label, x, y, radius) {
237         x = x || 15;
238         y = y || 15;
239         Shape.call(this, label, x, y);
240         this.radius = radius || 15;
241     }
242     Circle.prototype = new Shape();
244     Circle.prototype.getType = function() {
245         return 'circle';
246     };
248     Circle.prototype.getCoordinates = function() {
249         return this.centre + ';' + Math.abs(this.radius);
250     };
252     Circle.prototype.makeSvg = function(svg) {
253         var svgEl = createSvgShapeGroup(svg, 'circle');
254         this.updateSvg(svgEl);
255         return svgEl;
256     };
258     Circle.prototype.updateSvg = function(svgEl) {
259         svgEl.childNodes[0].setAttribute('cx', this.centre.x);
260         svgEl.childNodes[0].setAttribute('cy', this.centre.y);
261         svgEl.childNodes[0].setAttribute('r', Math.abs(this.radius));
262         svgEl.childNodes[1].setAttribute('x', this.centre.x);
263         svgEl.childNodes[1].setAttribute('y', this.centre.y + 15);
264         svgEl.childNodes[1].textContent = this.label;
265     };
267     Circle.prototype.parse = function(coordinates) {
268         if (!coordinates.match(/^\d+,\d+;\d+$/)) {
269             return false;
270         }
272         var bits = coordinates.split(';');
273         this.centre = Point.parse(bits[0]);
274         this.radius = Math.round(bits[1]);
275         return true;
276     };
278     Circle.prototype.move = function(dx, dy, maxX, maxY) {
279         this.centre.move(dx, dy);
280         if (this.centre.x < this.radius) {
281             this.centre.x = this.radius;
282         }
283         if (this.centre.x > maxX - this.radius) {
284             this.centre.x = maxX - this.radius;
285         }
286         if (this.centre.y < this.radius) {
287             this.centre.y = this.radius;
288         }
289         if (this.centre.y > maxY - this.radius) {
290             this.centre.y = maxY - this.radius;
291         }
292     };
294     Circle.prototype.edit = function(handleIndex, dx, dy, maxX, maxY) {
295         this.radius += dx;
296         var limit = Math.min(this.centre.x, this.centre.y, maxX - this.centre.x, maxY - this.centre.y);
297         if (this.radius > limit) {
298             this.radius = limit;
299         }
300         if (this.radius < -limit) {
301             this.radius = -limit;
302         }
303     };
305     /**
306      * Update the properties of this shape after a sequence of edits.
307      *
308      * For example make sure the circle radius is positive, of the polygon centre is centred.
309      */
310     Circle.prototype.normalizeShape = function() {
311         this.radius = Math.abs(this.radius);
312     };
314     Circle.prototype.makeSimilarRectangle = function() {
315         return new Rectangle(this.label,
316                 this.centre.x - this.radius, this.centre.y - this.radius,
317                 this.radius * 2, this.radius * 2);
318     };
320     Circle.prototype.makeSimilarPolygon = function() {
321         // We make a similar square, so if you go to and from Rectangle afterwards, it is loss-less.
322         return new Polygon(this.label, [
323                 this.centre.offset(-this.radius, -this.radius), this.centre.offset(-this.radius, this.radius),
324                 this.centre.offset(this.radius, this.radius), this.centre.offset(this.radius, -this.radius)]);
325     };
327     Circle.prototype.getHandlePositions = function() {
328         return {
329             moveHandle: this.centre,
330             editHandles: [this.centre.offset(this.radius, 0)]
331         };
332     };
335     /**
336      * A shape that is a rectangle.
337      *
338      * @param {String} label name of this area.
339      * @param {int} [x] top left X.
340      * @param {int} [y] top left Y.
341      * @param {int} [width] width.
342      * @param {int} [height] height.
343      * @constructor
344      */
345     function Rectangle(label, x, y, width, height) {
346         Shape.call(this, label, x, y);
347         this.width = width || 30;
348         this.height = height || 30;
349     }
350     Rectangle.prototype = new Shape();
352     Rectangle.prototype.getType = function() {
353         return 'rectangle';
354     };
356     Rectangle.prototype.getCoordinates = function() {
357         return this.centre + ';' + this.width + ',' + this.height;
358     };
360     Rectangle.prototype.makeSvg = function(svg) {
361         var svgEl = createSvgShapeGroup(svg, 'rect');
362         this.updateSvg(svgEl);
363         return svgEl;
364     };
366     Rectangle.prototype.updateSvg = function(svgEl) {
367         if (this.width >= 0) {
368             svgEl.childNodes[0].setAttribute('x', this.centre.x);
369             svgEl.childNodes[0].setAttribute('width', this.width);
370         } else {
371             svgEl.childNodes[0].setAttribute('x', this.centre.x + this.width);
372             svgEl.childNodes[0].setAttribute('width', -this.width);
373         }
374         if (this.height >= 0) {
375             svgEl.childNodes[0].setAttribute('y', this.centre.y);
376             svgEl.childNodes[0].setAttribute('height', this.height);
377         } else {
378             svgEl.childNodes[0].setAttribute('y', this.centre.y + this.height);
379             svgEl.childNodes[0].setAttribute('height', -this.height);
380         }
382         svgEl.childNodes[1].setAttribute('x', this.centre.x + this.width / 2);
383         svgEl.childNodes[1].setAttribute('y', this.centre.y + this.height / 2 + 15);
384         svgEl.childNodes[1].textContent = this.label;
385     };
387     Rectangle.prototype.parse = function(coordinates) {
388         if (!coordinates.match(/^\d+,\d+;\d+,\d+$/)) {
389             return false;
390         }
392         var bits = coordinates.split(';');
393         this.centre = Point.parse(bits[0]);
394         var size = Point.parse(bits[1]);
395         this.width = size.x;
396         this.height = size.y;
397         return true;
398     };
400     Rectangle.prototype.move = function(dx, dy, maxX, maxY) {
401         this.centre.move(dx, dy);
402         if (this.centre.x < 0) {
403             this.centre.x = 0;
404         }
405         if (this.centre.x > maxX - this.width) {
406             this.centre.x = maxX - this.width;
407         }
408         if (this.centre.y < 0) {
409             this.centre.y = 0;
410         }
411         if (this.centre.y > maxY - this.height) {
412             this.centre.y = maxY - this.height;
413         }
414     };
416     Rectangle.prototype.edit = function(handleIndex, dx, dy, maxX, maxY) {
417         this.width += dx;
418         this.height += dy;
419         if (this.width < -this.centre.x) {
420             this.width = -this.centre.x;
421         }
422         if (this.width > maxX - this.centre.x) {
423             this.width = maxX - this.centre.x;
424         }
425         if (this.height < -this.centre.y) {
426             this.height = -this.centre.y;
427         }
428         if (this.height > maxY - this.centre.y) {
429             this.height = maxY - this.centre.y;
430         }
431     };
433     /**
434      * Update the properties of this shape after a sequence of edits.
435      *
436      * For example make sure the circle radius is positive, of the polygon centre is centred.
437      */
438     Rectangle.prototype.normalizeShape = function() {
439         if (this.width < 0) {
440             this.centre.x += this.width;
441             this.width = -this.width;
442         }
443         if (this.height < 0) {
444             this.centre.y += this.height;
445             this.height = -this.height;
446         }
447     };
449     Rectangle.prototype.makeSimilarCircle = function() {
450         return new Circle(this.label,
451                 Math.round(this.centre.x + this.width / 2),
452                 Math.round(this.centre.y + this.height / 2),
453                 Math.round((this.width + this.height) / 4));
454     };
456     Rectangle.prototype.makeSimilarPolygon = function() {
457         return new Polygon(this.label, [
458             this.centre, this.centre.offset(0, this.height),
459             this.centre.offset(this.width, this.height), this.centre.offset(this.width, 0)]);
460     };
462     Rectangle.prototype.getHandlePositions = function() {
463         return {
464             moveHandle: this.centre.offset(this.width / 2, this.height / 2),
465             editHandles: [this.centre.offset(this.width, this.height)]
466         };
467     };
470     /**
471      * A shape that is a polygon.
472      *
473      * @param {String} label name of this area.
474      * @param {Point[]} [points] position of the vertices relative to (centreX, centreY).
475      *      each object in the array should have two
476      * @constructor
477      */
478     function Polygon(label, points) {
479         Shape.call(this, label, 0, 0);
480         this.points = points ? points.slice() : [new Point(10, 10), new Point(40, 10), new Point(10, 40)];
481         this.normalizeShape();
482     }
483     Polygon.prototype = new Shape();
485     Polygon.prototype.getType = function() {
486         return 'polygon';
487     };
489     Polygon.prototype.getCoordinates = function() {
490         var coordinates = '';
491         for (var i = 0; i < this.points.length; i++) {
492             coordinates += this.centre.offset(this.points[i]) + ';';
493         }
494         return coordinates.slice(0, coordinates.length - 1); // Strip off the last ';'.
495     };
497     Polygon.prototype.makeSvg = function(svg) {
498         var svgEl = createSvgShapeGroup(svg, 'polygon');
499         this.updateSvg(svgEl);
500         return svgEl;
501     };
503     Polygon.prototype.updateSvg = function(svgEl) {
504         svgEl.childNodes[0].setAttribute('points', this.getCoordinates().replace(/[,;]/g, ' '));
505         svgEl.childNodes[1].setAttribute('x', this.centre.x);
506         svgEl.childNodes[1].setAttribute('y', this.centre.y + 15);
507         svgEl.childNodes[1].textContent = this.label;
508     };
510     Polygon.prototype.parse = function(coordinates) {
511         if (!coordinates.match(/^\d+,\d+(?:;\d+,\d+)*$/)) {
512             return false;
513         }
515         var bits = coordinates.split(';');
516         var points = [];
517         for (var i = 0; i < bits.length; i++) {
518             points.push(Point.parse(bits[i]));
519         }
521         this.points = points;
522         this.centre.x = 0;
523         this.centre.y = 0;
524         this.normalizeShape();
526         return true;
527     };
529     Polygon.prototype.move = function(dx, dy, maxX, maxY) {
530         this.centre.move(dx, dy);
531         var bbXMin = maxX,
532             bbXMax = 0,
533             bbYMin = maxY,
534             bbYMax = 0;
535         // Computer centre.
536         for (var i = 0; i < this.points.length; i++) {
537             bbXMin = Math.min(bbXMin, this.points[i].x);
538             bbXMax = Math.max(bbXMax, this.points[i].x);
539             bbYMin = Math.min(bbYMin, this.points[i].y);
540             bbYMax = Math.max(bbYMax, this.points[i].y);
541         }
542         if (this.centre.x < -bbXMin) {
543             this.centre.x = -bbXMin;
544         }
545         if (this.centre.x > maxX - bbXMax) {
546             this.centre.x = maxX - bbXMax;
547         }
548         if (this.centre.y < -bbYMin) {
549             this.centre.y = -bbYMin;
550         }
551         if (this.centre.y > maxY - bbYMax) {
552             this.centre.y = maxY - bbYMax;
553         }
554     };
556     Polygon.prototype.edit = function(handleIndex, dx, dy, maxX, maxY) {
557         this.points[handleIndex].move(dx, dy);
558         if (this.points[handleIndex].x < -this.centre.x) {
559             this.points[handleIndex].x = -this.centre.x;
560         }
561         if (this.points[handleIndex].x > maxX - this.centre.x) {
562             this.points[handleIndex].x = maxX - this.centre.x;
563         }
564         if (this.points[handleIndex].y < -this.centre.y) {
565             this.points[handleIndex].y = -this.centre.y;
566         }
567         if (this.points[handleIndex].y > maxY - this.centre.y) {
568             this.points[handleIndex].y = maxY - this.centre.y;
569         }
570     };
572     /**
573      * Add a new point after the given point, with the same co-ordinates.
574      *
575      * This does not automatically normalise.
576      *
577      * @param {int} pointIndex the index of the vertex after which to insert this new one.
578      */
579     Polygon.prototype.addNewPointAfter = function(pointIndex) {
580         this.points.splice(pointIndex, 0,
581                 new Point(this.points[pointIndex].x, this.points[pointIndex].y));
582     };
584     Polygon.prototype.normalizeShape = function() {
585         var i,
586             x = 0,
587             y = 0;
589         if (this.points.length === 0) {
590             return;
591         }
593         // Computer centre.
594         for (i = 0; i < this.points.length; i++) {
595             x += this.points[i].x;
596             y += this.points[i].y;
597         }
598         x = Math.round(x / this.points.length);
599         y = Math.round(y / this.points.length);
601         if (x === 0 && y === 0) {
602             return;
603         }
605         for (i = 0; i < this.points.length; i++) {
606             this.points[i].move(-x, -y);
607         }
608         this.centre.move(x, y);
609     };
611     Polygon.prototype.makeSimilarCircle = function() {
612         return this.makeSimilarRectangle().makeSimilarCircle();
613     };
615     Polygon.prototype.makeSimilarRectangle = function() {
616         var p,
617             minX = 0,
618             maxX = 0,
619             minY = 0,
620             maxY = 0;
621         for (var i = 0; i < this.points.length; i++) {
622             p = this.points[i];
623             minX = Math.min(minX, p.x);
624             maxX = Math.max(maxX, p.x);
625             minY = Math.min(minY, p.y);
626             maxY = Math.max(maxY, p.y);
627         }
628         return new Rectangle(this.label,
629                 this.centre.x + minX, this.centre.y + minY,
630                 Math.max(maxX - minX, 10), Math.max(maxY - minY, 10));
631     };
633     Polygon.prototype.getHandlePositions = function() {
634         var editHandles = [];
635         for (var i = 0; i < this.points.length; i++) {
636             editHandles.push(this.points[i].offset(this.centre.x, this.centre.y));
637         }
639         return {
640             moveHandle: this.centre,
641             editHandles: editHandles
642         };
643     };
646     /**
647      * Not a shape (null object pattern).
648      *
649      * @param {String} label name of this area.
650      * @constructor
651      */
652     function NullShape(label) {
653         Shape.call(this, label);
654     }
655     NullShape.prototype = new Shape();
657     NullShape.prototype.getType = function() {
658         return 'null';
659     };
661     NullShape.prototype.getCoordinates = function() {
662         return '';
663     };
665     NullShape.prototype.makeSvg = function(svg) {
666         void (svg);
667         return null;
668     };
670     NullShape.prototype.updateSvg = function(svgEl) {
671         void (svgEl);
672     };
674     NullShape.prototype.parse = function(coordinates) {
675         void (coordinates);
676         return false;
677     };
679     NullShape.prototype.makeSimilarCircle = function() {
680         return new Circle(this.label);
681     };
683     NullShape.prototype.makeSimilarRectangle = function() {
684         return new Rectangle(this.label);
685     };
687     NullShape.prototype.makeSimilarPolygon = function() {
688         return new Polygon(this.label);
689     };
692     /**
693      * Make a new SVG DOM element as a child of svg.
694      *
695      * @param {SVGElement} svg the parent node.
696      * @param {String} tagName the tag name.
697      * @return {SVGElement} the newly created node.
698      */
699     function createSvgElement(svg, tagName) {
700         var svgEl = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', tagName);
701         svg.appendChild(svgEl);
702         return svgEl;
703     }
705     /**
706      * Make a group SVG DOM elements containing a shape of the given type as first child,
707      * and a text label as the second child.
708      *
709      * @param {SVGElement} svg the parent node.
710      * @param {String} tagName the tag name.
711      * @return {SVGElement} the newly created g element.
712      */
713     function createSvgShapeGroup(svg, tagName) {
714         var svgEl = createSvgElement(svg, 'g');
715         createSvgElement(svgEl, tagName).setAttribute('class', 'shape');
716         createSvgElement(svgEl, 'text').setAttribute('class', 'shapeLabel');
717         return svgEl;
718     }
720     /**
721      * @alias module:qtype_ddmarker/shapes
722      */
723     return {
724         /**
725          * A point, with x and y coordinates.
726          *
727          * @param {int} x centre X.
728          * @param {int} y centre Y.
729          * @constructor
730          */
731         Point: Point,
733         /**
734          * A point, with x and y coordinates.
735          *
736          * @param {int} x centre X.
737          * @param {int} y centre Y.
738          * @constructor
739          */
740         Shape: Shape,
742         /**
743          * A shape that is a circle.
744          *
745          * @param {String} label name of this area.
746          * @param {int} [x] centre X.
747          * @param {int} [y] centre Y.
748          * @param {int} [radius] radius.
749          * @constructor
750          */
751         Circle: Circle,
753         /**
754          * A shape that is a rectangle.
755          *
756          * @param {String} label name of this area.
757          * @param {int} [x] top left X.
758          * @param {int} [y] top left Y.
759          * @param {int} [width] width.
760          * @param {int} [height] height.
761          * @constructor
762          */
763         Rectangle: Rectangle,
765         /**
766          * A shape that is a polygon.
767          *
768          * @param {String} label name of this area.
769          * @param {Point[]} [points] position of the vertices relative to (centreX, centreY).
770          *      each object in the array should have two
771          * @constructor
772          */
773         Polygon: Polygon,
775         /**
776          * Not a shape (null object pattern).
777          *
778          * @param {String} label name of this area.
779          * @constructor
780          */
781         NullShape: NullShape,
783         /**
784          * Make a new SVG DOM element as a child of svg.
785          *
786          * @param {SVGElement} svg the parent node.
787          * @param {String} tagName the tag name.
788          * @return {SVGElement} the newly created node.
789          */
790         createSvgElement: createSvgElement,
792         /**
793          * Make a shape of the given type.
794          *
795          * @param {String} shapeType
796          * @param {String} label
797          * @return {Shape} the requested shape.
798          */
799         make: function(shapeType, label) {
800             switch (shapeType) {
801                 case 'circle':
802                     return new Circle(label);
803                 case 'rectangle':
804                     return new Rectangle(label);
805                 case 'polygon':
806                     return new Polygon(label);
807                 default:
808                     return new NullShape(label);
809             }
810         },
812         /**
813          * Make a shape of the given type that is similar to the shape of the original type.
814          *
815          * @param {String} shapeType the new type of shape to make
816          * @param {Shape} shape the shape to copy
817          * @return {Shape} the similar shape of a different type.
818          */
819         getSimilar: function(shapeType, shape) {
820             if (shapeType === shape.getType()) {
821                 return shape;
822             }
823             switch (shapeType) {
824                 case 'circle':
825                     return shape.makeSimilarCircle();
826                 case 'rectangle':
827                     return shape.makeSimilarRectangle();
828                 case 'polygon':
829                     return shape.makeSimilarPolygon();
830                 default:
831                     return new NullShape(shape.label);
832             }
833         }
834     };
835 });