1 /*
  2     Copyright 2008-2013
  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  base/element
 40  utils/type
 41   elements:
 42    curve
 43    point
 44    line
 45    transform
 46  */
 47 
 48 /**
 49  * @fileoverview The JSXGraph object Turtle is defined. It acts like
 50  * "turtle graphics".
 51  * @author A.W.
 52  */
 53 
 54 define([
 55     'jxg', 'base/constants', 'base/element', 'utils/type', 'base/curve', 'base/point', 'base/line', 'base/transformation'
 56 ], function (JXG, Const, GeometryElement, Type, Curve, Point, Line, Transform) {
 57 
 58     "use strict";
 59 
 60     /**
 61      * Constructs a new Turtle object.
 62      * @class This is the Turtle class.
 63      * It is derived from {@link JXG.GeometryElement}.
 64      * It stores all properties required
 65      * to move a turtle.
 66      * @constructor
 67      * @param {JXG.Board} board The board the new turtle is drawn on.
 68      * @param {Array} parents Start position and start direction of the turtle. Possible values are
 69      * [x, y, angle]
 70      * [[x, y], angle]
 71      * [x, y]
 72      * [[x, y]]
 73      * @param {Object} attributes Attributes to change the visual properties of the turtle object
 74      * All angles are in degrees.
 75      */
 76     JXG.Turtle = function (board, parents, attributes) {
 77         var x, y, dir;
 78 
 79         this.constructor(board, attributes, Const.OBJECT_TYPE_TURTLE, Const.OBJECT_CLASS_OTHER);
 80 
 81         this.turtleIsHidden = false;
 82         this.board = board;
 83         this.visProp.curveType = 'plot';
 84 
 85         // Save visProp in this._attributes.
 86         // this._attributes is overwritten by setPenSize, setPenColor...
 87         // Setting the color or size affects the turtle from the time of
 88         // calling the method,
 89         // whereas Turtle.setAttribute affects all turtle curves.
 90         this._attributes = Type.copyAttributes(this.visProp, board.options, 'turtle');
 91         delete this._attributes.id;
 92 
 93         x = 0;
 94         y = 0;
 95         dir = 90;
 96 
 97         if (parents.length !== 0) {
 98             // [x,y,dir]
 99             if (parents.length === 3) {
100                 // Only numbers are accepted at the moment
101                 x = parents[0];
102                 y = parents[1];
103                 dir = parents[2];
104             } else if (parents.length === 2) {
105                 // [[x,y],dir]
106                 if (Type.isArray(parents[0])) {
107                     x = parents[0][0];
108                     y = parents[0][1];
109                     dir = parents[1];
110                 // [x,y]
111                 } else {
112                     x = parents[0];
113                     y = parents[1];
114                 }
115             // [[x,y]]
116             } else {
117                 x = parents[0][0];
118                 y = parents[0][1];
119             }
120         }
121 
122         this.init(x, y, dir);
123 
124         this.methodMap = Type.deepCopy(this.methodMap, {
125             forward: 'forward',
126             fd: 'forward',
127             back: 'back',
128             bk: 'back',
129             right: 'right',
130             rt: 'right',
131             left: 'left',
132             lt: 'left',
133             penUp: 'penUp',
134             pu: 'penUp',
135             penDown: 'penDown',
136             pd: 'penDown',
137             clearScreen: 'clearScreen',
138             cs: 'clearScreen',
139             clean: 'clean',
140             setPos: 'setPos',
141             home: 'home',
142             hideTurtle: 'hideTurtle',
143             ht: 'hideTurtle',
144             showTurtle: 'showTurtle',
145             st: 'showTurtle',
146             penSize: 'setPenSize',
147             penColor: 'setPenColor',
148             pushTurtle: 'pushTurtle',
149             push: 'pushTurtle',
150             popTurtle: 'popTurtle',
151             pop: 'popTurtle',
152             lookTo: 'lookTo',
153             pos: 'pos',
154             moveTo: 'moveTo',
155             X: 'X',
156             Y: 'Y'
157         });
158 
159         return this;
160     };
161 
162     JXG.Turtle.prototype = new GeometryElement();
163 
164     JXG.extend(JXG.Turtle.prototype, /** @lends JXG.Turtle.prototype */ {
165         /**
166          * Initialize a new turtle or reinitialize a turtle after {@link JXG.Turtle#clearScreen}.
167          * @private
168          */
169         init: function (x, y, dir) {
170             var hiddenPointAttr = {
171                     fixed: true,
172                     name: '',
173                     visible: false,
174                     withLabel: false
175                 };
176 
177             this.arrowLen = 20 / Math.sqrt(this.board.unitX * this.board.unitX + this.board.unitY * this.board.unitY);
178 
179             this.pos = [x, y];
180             this.isPenDown = true;
181             this.dir = 90;
182             this.stack = [];
183             this.objects = [];
184             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
185             this.objects.push(this.curve);
186 
187             this.turtle = this.board.create('point', this.pos, hiddenPointAttr);
188             this.objects.push(this.turtle);
189 
190             this.turtle2 = this.board.create('point', [this.pos[0], this.pos[1] + this.arrowLen], hiddenPointAttr);
191             this.objects.push(this.turtle2);
192 
193             this.visProp.arrow.lastArrow = true;
194             this.visProp.arrow.straightFirst = false;
195             this.visProp.arrow.straightLast = false;
196             this.arrow = this.board.create('line', [this.turtle, this.turtle2], this.visProp.arrow);
197             this.objects.push(this.arrow);
198 
199             this.right(90 - dir);
200             this.board.update();
201         },
202 
203         /**
204          * Move the turtle forward.
205          * @param {Number} len of forward move in user coordinates
206          * @returns {JXG.Turtle} pointer to the turtle object
207          */
208         forward: function (len) {
209             if (len === 0) {
210                 return this;
211             }
212 
213             var t,
214                 dx = len * Math.cos(this.dir * Math.PI / 180),
215                 dy = len * Math.sin(this.dir * Math.PI / 180);
216 
217             if (!this.turtleIsHidden) {
218                 t = this.board.create('transform', [dx, dy], {type: 'translate'});
219 
220                 t.applyOnce(this.turtle);
221                 t.applyOnce(this.turtle2);
222             }
223 
224             if (this.isPenDown) {
225                 // IE workaround
226                 if (this.curve.dataX.length >= 8192) {
227                     this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
228                     this.objects.push(this.curve);
229                 }
230             }
231 
232             this.pos[0] += dx;
233             this.pos[1] += dy;
234 
235             if (this.isPenDown) {
236                 this.curve.dataX.push(this.pos[0]);
237                 this.curve.dataY.push(this.pos[1]);
238             }
239 
240             this.board.update();
241             return this;
242         },
243 
244         /**
245          * Move the turtle backwards.
246          * @param {Number} len of backwards move in user coordinates
247          * @returns {JXG.Turtle} pointer to the turtle object
248          */
249         back: function (len) {
250             return this.forward(-len);
251         },
252 
253         /**
254          * Rotate the turtle direction to the right
255          * @param {Number} angle of the rotation in degrees
256          * @returns {JXG.Turtle} pointer to the turtle object
257          */
258         right: function (angle) {
259             this.dir -= angle;
260             this.dir %= 360;
261 
262             if (!this.turtleIsHidden) {
263                 var t = this.board.create('transform', [-angle * Math.PI / 180, this.turtle], {type: 'rotate'});
264                 t.applyOnce(this.turtle2);
265             }
266 
267             this.board.update();
268             return this;
269         },
270 
271         /**
272          * Rotate the turtle direction to the right.
273          * @param {Number} angle of the rotation in degrees
274          * @returns {JXG.Turtle} pointer to the turtle object
275          */
276         left: function (angle) {
277             return this.right(-angle);
278         },
279 
280         /**
281          * Pen up, stops visible drawing
282          * @returns {JXG.Turtle} pointer to the turtle object
283          */
284         penUp: function () {
285             this.isPenDown = false;
286             return this;
287         },
288 
289         /**
290          * Pen down, continues visible drawing
291          * @returns {JXG.Turtle} pointer to the turtle object
292          */
293         penDown: function () {
294             this.isPenDown = true;
295             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
296             this.objects.push(this.curve);
297 
298             return this;
299         },
300 
301         /**
302          * Removes the turtle curve from the board. The turtle stays in its position.
303          * @returns {JXG.Turtle} pointer to the turtle object
304          */
305         clean: function () {
306             var i, el;
307 
308             for (i = 0; i < this.objects.length; i++) {
309                 el = this.objects[i];
310                 if (el.type === Const.OBJECT_TYPE_CURVE) {
311                     this.board.removeObject(el);
312                     this.objects.splice(i, 1);
313                 }
314             }
315 
316             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
317             this.objects.push(this.curve);
318             this.board.update();
319 
320             return this;
321         },
322 
323         /**
324          *  Removes the turtle completely and resets it to its initial position and direction.
325          * @returns {JXG.Turtle} pointer to the turtle object
326          */
327         clearScreen: function () {
328             var i, el,
329                 len = this.objects.length;
330 
331             for (i = 0; i < len; i++) {
332                 el = this.objects[i];
333                 this.board.removeObject(el);
334             }
335 
336             this.init(0, 0, 90);
337             return this;
338         },
339 
340         /**
341          *  Moves the turtle without drawing to a new position
342          * @param {Number} x new x- coordinate
343          * @param {Number} y new y- coordinate
344          * @returns {JXG.Turtle} pointer to the turtle object
345          */
346         setPos: function (x, y) {
347             var t;
348 
349             if (Type.isArray(x)) {
350                 this.pos = x;
351             } else {
352                 this.pos = [x, y];
353             }
354 
355             if (!this.turtleIsHidden) {
356                 this.turtle.setPositionDirectly(Const.COORDS_BY_USER, [x, y]);
357                 this.turtle2.setPositionDirectly(Const.COORDS_BY_USER, [x, y + this.arrowLen]);
358                 t = this.board.create('transform', [-(this.dir - 90) * Math.PI / 180, this.turtle], {type: 'rotate'});
359                 t.applyOnce(this.turtle2);
360             }
361 
362             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
363             this.objects.push(this.curve);
364             this.board.update();
365 
366             return this;
367         },
368 
369         /**
370          *  Sets the pen size. Equivalent to setAttribute({strokeWidth:size})
371          * but affects only the future turtle.
372          * @param {Number} size
373          * @returns {JXG.Turtle} pointer to the turtle object
374          */
375         setPenSize: function (size) {
376             //this.visProp.strokewidth = size;
377             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this.copyAttr('strokeWidth', size));
378             this.objects.push(this.curve);
379             return this;
380         },
381 
382         /**
383          *  Sets the pen color. Equivalent to setAttribute({strokeColor:color})
384          * but affects only the future turtle.
385          * @param {String} color
386          * @returns {JXG.Turtle} pointer to the turtle object
387          */
388         setPenColor: function (color) {
389             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this.copyAttr('strokeColor', color));
390             this.objects.push(this.curve);
391 
392             return this;
393         },
394 
395         /**
396          *  Sets the highlight pen color. Equivalent to setAttribute({highlightStrokeColor:color})
397          * but affects only the future turtle.
398          * @param {String} color
399          * @returns {JXG.Turtle} pointer to the turtle object
400          */
401         setHighlightPenColor: function (color) {
402             //this.visProp.highlightstrokecolor = colStr;
403             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this.copyAttr('highlightStrokeColor', color));
404             this.objects.push(this.curve);
405             return this;
406         },
407 
408         /**
409          * Sets properties of the turtle, see also {@link JXG.GeometryElement#setAttribute}.
410          * Sets the property for all curves of the turtle in the past and in the future.
411          * @param {Object} attributes key:value pairs
412          * @returns {JXG.Turtle} pointer to the turtle object
413          */
414         setAttribute: function (attributes) {
415             var i, el, tmp,
416                 len = this.objects.length;
417 
418             for (i = 0; i < len; i++) {
419                 el = this.objects[i];
420                 if (el.type === Const.OBJECT_TYPE_CURVE) {
421                     el.setAttribute(attributes);
422                 }
423             }
424 
425             // Set visProp of turtle
426             tmp = this.visProp.id;
427             this.visProp = Type.deepCopy(this.curve.visProp);
428             this.visProp.id = tmp;
429             this._attributes = Type.deepCopy(this.visProp);
430             delete this._attributes.id;
431 
432             return this;
433         },
434 
435         /**
436          * Set a future attribute of the turtle.
437          * @private
438          * @param {String} key
439          * @param {Number|String} val
440          * @returns {Object} pointer to the attributes object
441          */
442         copyAttr: function (key, val) {
443             this._attributes[key.toLowerCase()] = val;
444             return this._attributes;
445         },
446 
447         /**
448          * Sets the visibility of the turtle head to true,
449          * @returns {JXG.Turtle} pointer to the turtle object
450          */
451         showTurtle: function () {
452             this.turtleIsHidden = false;
453             this.arrow.setAttribute({visible: true});
454             this.visProp.arrow.visible = false;
455             this.setPos(this.pos[0], this.pos[1]);
456             this.board.update();
457 
458             return this;
459         },
460 
461         /**
462          * Sets the visibility of the turtle head to false,
463          * @returns {JXG.Turtle} pointer to the turtle object
464          */
465         hideTurtle: function () {
466             this.turtleIsHidden = true;
467             this.arrow.setAttribute({visible: false});
468             this.visProp.arrow.visible = false;
469             this.board.update();
470 
471             return this;
472         },
473 
474         /**
475          * Moves the turtle to position [0,0].
476          * @returns {JXG.Turtle} pointer to the turtle object
477          */
478         home: function () {
479             this.pos = [0, 0];
480             this.setPos(this.pos[0], this.pos[1]);
481 
482             return this;
483         },
484 
485         /**
486          *  Pushes the position of the turtle on the stack.
487          * @returns {JXG.Turtle} pointer to the turtle object
488          */
489         pushTurtle: function () {
490             this.stack.push([this.pos[0], this.pos[1], this.dir]);
491 
492             return this;
493         },
494 
495         /**
496          *  Gets the last position of the turtle on the stack, sets the turtle to this position and removes this
497          * position from the stack.
498          * @returns {JXG.Turtle} pointer to the turtle object
499          */
500         popTurtle: function () {
501             var status = this.stack.pop();
502             this.pos[0] = status[0];
503             this.pos[1] = status[1];
504             this.dir = status[2];
505             this.setPos(this.pos[0], this.pos[1]);
506 
507             return this;
508         },
509 
510         /**
511          * Rotates the turtle into a new direction.
512          * There are two possibilities:
513          * @param {Number|Array} target If a number is given, it is interpreted as the new direction to look to; If an array
514          * consisting of two Numbers is given targeted is used as a pair coordinates.
515          * @returns {JXG.Turtle} pointer to the turtle object
516          */
517         lookTo: function (target) {
518             var ax, ay, bx, by, beta;
519 
520             if (Type.isArray(target)) {
521                 ax = this.pos[0];
522                 ay = this.pos[1];
523                 bx = target[0];
524                 by = target[1];
525 
526                 // Rotate by the slope of the line [this.pos, target]
527                 beta = Math.atan2(by - ay, bx - ax);
528                 this.right(this.dir - (beta * 180 / Math.PI));
529             } else if (Type.isNumber(target)) {
530                 this.right(this.dir - target);
531             }
532             return this;
533         },
534 
535         /**
536          * Moves the turtle to a given coordinate pair.
537          * The direction is not changed.
538          * @param {Array} target Coordinates of the point where the turtle looks to.
539          * @returns {JXG.Turtle} pointer to the turtle object
540          */
541         moveTo: function (target) {
542             var dx, dy, t;
543 
544             if (Type.isArray(target)) {
545                 dx = target[0] - this.pos[0];
546                 dy = target[1] - this.pos[1];
547 
548                 if (!this.turtleIsHidden) {
549                     t = this.board.create('transform', [dx, dy], {type: 'translate'});
550                     t.applyOnce(this.turtle);
551                     t.applyOnce(this.turtle2);
552                 }
553 
554                 if (this.isPenDown) {
555                     // IE workaround
556                     if (this.curve.dataX.length >= 8192) {
557                         this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
558                         this.objects.push(this.curve);
559                     }
560                 }
561 
562                 this.pos[0] = target[0];
563                 this.pos[1] = target[1];
564 
565                 if (this.isPenDown) {
566                     this.curve.dataX.push(this.pos[0]);
567                     this.curve.dataY.push(this.pos[1]);
568                 }
569                 this.board.update();
570             }
571 
572             return this;
573         },
574 
575         /**
576          * Alias for {@link #forward}
577          */
578         fd: function (len) { return this.forward(len); },
579         /**
580          * Alias for {@link #back}
581          */
582         bk: function (len) { return this.back(len); },
583         /**
584          * Alias for {@link #left}
585          */
586         lt: function (angle) { return this.left(angle); },
587         /**
588          * Alias for {@link #right}
589          */
590         rt: function (angle) { return this.right(angle); },
591         /**
592          * Alias for {@link #penUp}
593          */
594         pu: function () { return this.penUp(); },
595         /**
596          * Alias for {@link #penDown}
597          */
598         pd: function () { return this.penDown(); },
599         /**
600          * Alias for {@link #hideTurtle}
601          */
602         ht: function () { return this.hideTurtle(); },
603         /**
604          * Alias for {@link #showTurtle}
605          */
606         st: function () { return this.showTurtle(); },
607         /**
608          * Alias for {@link #clearScreen}
609          */
610         cs: function () { return this.clearScreen(); },
611         /**
612          * Alias for {@link #pushTurtle}
613          */
614         push: function () { return this.pushTurtle(); },
615         /**
616          * Alias for {@link #popTurtle}
617          */
618         pop: function () { return this.popTurtle(); },
619 
620         /**
621          * the "co"-coordinate of the turtle curve at position t is returned.
622          * @param {Number} t parameter
623          * @param {String} co. Either 'X' or 'Y'.
624          * @returns {Number} x-coordinate of the turtle position or x-coordinate of turtle at position t
625          */
626         evalAt: function (t, co) {
627             var i, j, el, tc,
628                 len = this.objects.length;
629 
630             for (i = 0, j = 0; i < len; i++) {
631                 el = this.objects[i];
632 
633                 if (el.elementClass === Const.OBJECT_CLASS_CURVE) {
634                     if (j <= t && t < j + el.numberPoints) {
635                         tc = (t - j);
636                         return el[co](tc);
637                     }
638                     j += el.numberPoints;
639                 }
640             }
641 
642             return this[co]();
643         },
644 
645         /**
646          * if t is not supplied the x-coordinate of the turtle is returned. Otherwise
647          * the x-coordinate of the turtle curve at position t is returned.
648          * @param {Number} t parameter
649          * @returns {Number} x-coordinate of the turtle position or x-coordinate of turtle at position t
650          */
651         X: function (t) {
652             if (!Type.exists(t)) {
653                 return this.pos[0];
654             }
655 
656             return this.evalAt(t, 'X');
657         },
658 
659         /**
660          * if t is not supplied the y-coordinate of the turtle is returned. Otherwise
661          * the y-coordinate of the turtle curve at position t is returned.
662          * @param {Number} t parameter
663          * @returns {Number} x-coordinate of the turtle position or x-coordinate of turtle at position t
664          */
665         Y: function (t) {
666             if (!Type.exists(t)) {
667                 return this.pos[1];
668             }
669             return this.evalAt(t, 'Y');
670         },
671 
672         /**
673          * @returns {Number} z-coordinate of the turtle position
674          */
675         Z: function (t) {
676             return 1.0;
677         },
678 
679         /**
680          * Gives the lower bound of the parameter if the the turtle is treated as parametric curve.
681          */
682         minX: function () {
683             return 0;
684         },
685 
686         /**
687          * Gives the upper bound of the parameter if the the turtle is treated as parametric curve.
688          * May be overwritten in @see generateTerm.
689          */
690         maxX: function () {
691             var i, el,
692                 len = this.objects.length,
693                 np = 0;
694 
695             for (i = 0; i < len; i++) {
696                 el = this.objects[i];
697                 if (el.elementClass === Const.OBJECT_CLASS_CURVE) {
698                     np += this.objects[i].numberPoints;
699                 }
700             }
701             return np;
702         },
703 
704         /**
705          * Checks whether (x,y) is near the curve.
706          * @param {Number} x Coordinate in x direction, screen coordinates.
707          * @param {Number} y Coordinate in y direction, screen coordinates.
708          * @returns {Boolean} True if (x,y) is near the curve, False otherwise.
709          */
710         hasPoint: function (x, y) {
711             var i, el;
712 
713             // run through all curves of this turtle
714             for (i = 0; i < this.objects.length; i++) {
715                 el = this.objects[i];
716 
717                 if (el.type === Const.OBJECT_TYPE_CURVE) {
718                     if (el.hasPoint(x, y)) {
719                         // So what??? All other curves have to be notified now (for highlighting)
720                         return true;
721                         // This has to be done, yet.
722                     }
723                 }
724             }
725             return false;
726         }
727     });
728 
729     /**
730      * Creates a new turtle
731      * @param {JXG.Board} board The board the turtle is put on.
732      * @param {Array} parents
733      * @param {Object} attributes Object containing properties for the element such as stroke-color and visibility. See {@link JXG.GeometryElement#setAttribute}
734      * @returns {JXG.Turtle} Reference to the created turtle object.
735      */
736     JXG.createTurtle = function (board, parents, attributes) {
737         var attr;
738         parents = parents || [];
739 
740         attr = Type.copyAttributes(attributes, board.options, 'turtle');
741         return new JXG.Turtle(board, parents, attr);
742     };
743 
744     JXG.registerElement('turtle', JXG.createTurtle);
745 
746     return {
747         Turtle: JXG.Turtle,
748         createTurtle: JXG.createTurtle
749     };
750 });
751