1 /*
  2     Copyright 2008-2018
  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.subs = {
200                 arrow: this.arrow
201             };
202             this.inherits.push(this.arrow);
203 
204             this.right(90 - dir);
205             this.board.update();
206         },
207 
208         /**
209          * Move the turtle forward.
210          * @param {Number} len of forward move in user coordinates
211          * @returns {JXG.Turtle} pointer to the turtle object
212          */
213         forward: function (len) {
214             if (len === 0) {
215                 return this;
216             }
217 
218             var t,
219                 dx = len * Math.cos(this.dir * Math.PI / 180),
220                 dy = len * Math.sin(this.dir * Math.PI / 180);
221 
222             if (!this.turtleIsHidden) {
223                 t = this.board.create('transform', [dx, dy], {type: 'translate'});
224 
225                 t.applyOnce(this.turtle);
226                 t.applyOnce(this.turtle2);
227             }
228 
229             if (this.isPenDown) {
230                 // IE workaround
231                 if (this.curve.dataX.length >= 8192) {
232                     this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
233                     this.objects.push(this.curve);
234                 }
235             }
236 
237             this.pos[0] += dx;
238             this.pos[1] += dy;
239 
240             if (this.isPenDown) {
241                 this.curve.dataX.push(this.pos[0]);
242                 this.curve.dataY.push(this.pos[1]);
243             }
244 
245             this.board.update();
246             return this;
247         },
248 
249         /**
250          * Move the turtle backwards.
251          * @param {Number} len of backwards move in user coordinates
252          * @returns {JXG.Turtle} pointer to the turtle object
253          */
254         back: function (len) {
255             return this.forward(-len);
256         },
257 
258         /**
259          * Rotate the turtle direction to the right
260          * @param {Number} angle of the rotation in degrees
261          * @returns {JXG.Turtle} pointer to the turtle object
262          */
263         right: function (angle) {
264             this.dir -= angle;
265             this.dir %= 360;
266 
267             if (!this.turtleIsHidden) {
268                 var t = this.board.create('transform', [-angle * Math.PI / 180, this.turtle], {type: 'rotate'});
269                 t.applyOnce(this.turtle2);
270             }
271 
272             this.board.update();
273             return this;
274         },
275 
276         /**
277          * Rotate the turtle direction to the right.
278          * @param {Number} angle of the rotation in degrees
279          * @returns {JXG.Turtle} pointer to the turtle object
280          */
281         left: function (angle) {
282             return this.right(-angle);
283         },
284 
285         /**
286          * Pen up, stops visible drawing
287          * @returns {JXG.Turtle} pointer to the turtle object
288          */
289         penUp: function () {
290             this.isPenDown = false;
291             return this;
292         },
293 
294         /**
295          * Pen down, continues visible drawing
296          * @returns {JXG.Turtle} pointer to the turtle object
297          */
298         penDown: function () {
299             this.isPenDown = true;
300             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
301             this.objects.push(this.curve);
302 
303             return this;
304         },
305 
306         /**
307          * Removes the turtle curve from the board. The turtle stays in its position.
308          * @returns {JXG.Turtle} pointer to the turtle object
309          */
310         clean: function () {
311             var i, el;
312 
313             for (i = 0; i < this.objects.length; i++) {
314                 el = this.objects[i];
315                 if (el.type === Const.OBJECT_TYPE_CURVE) {
316                     this.board.removeObject(el);
317                     this.objects.splice(i, 1);
318                 }
319             }
320 
321             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
322             this.objects.push(this.curve);
323             this.board.update();
324 
325             return this;
326         },
327 
328         /**
329          *  Removes the turtle completely and resets it to its initial position and direction.
330          * @returns {JXG.Turtle} pointer to the turtle object
331          */
332         clearScreen: function () {
333             var i, el,
334                 len = this.objects.length;
335 
336             for (i = 0; i < len; i++) {
337                 el = this.objects[i];
338                 this.board.removeObject(el);
339             }
340 
341             this.init(0, 0, 90);
342             return this;
343         },
344 
345         /**
346          *  Moves the turtle without drawing to a new position
347          * @param {Number} x new x- coordinate
348          * @param {Number} y new y- coordinate
349          * @returns {JXG.Turtle} pointer to the turtle object
350          */
351         setPos: function (x, y) {
352             var t;
353 
354             if (Type.isArray(x)) {
355                 this.pos = x;
356             } else {
357                 this.pos = [x, y];
358             }
359 
360             if (!this.turtleIsHidden) {
361                 this.turtle.setPositionDirectly(Const.COORDS_BY_USER, [x, y]);
362                 this.turtle2.setPositionDirectly(Const.COORDS_BY_USER, [x, y + this.arrowLen]);
363                 t = this.board.create('transform', [-(this.dir - 90) * Math.PI / 180, this.turtle], {type: 'rotate'});
364                 t.applyOnce(this.turtle2);
365             }
366 
367             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
368             this.objects.push(this.curve);
369             this.board.update();
370 
371             return this;
372         },
373 
374         /**
375          *  Sets the pen size. Equivalent to setAttribute({strokeWidth:size})
376          * but affects only the future turtle.
377          * @param {Number} size
378          * @returns {JXG.Turtle} pointer to the turtle object
379          */
380         setPenSize: function (size) {
381             //this.visProp.strokewidth = size;
382             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this.copyAttr('strokeWidth', size));
383             this.objects.push(this.curve);
384             return this;
385         },
386 
387         /**
388          *  Sets the pen color. Equivalent to setAttribute({strokeColor:color})
389          * but affects only the future turtle.
390          * @param {String} color
391          * @returns {JXG.Turtle} pointer to the turtle object
392          */
393         setPenColor: function (color) {
394             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this.copyAttr('strokeColor', color));
395             this.objects.push(this.curve);
396 
397             return this;
398         },
399 
400         /**
401          *  Sets the highlight pen color. Equivalent to setAttribute({highlightStrokeColor:color})
402          * but affects only the future turtle.
403          * @param {String} color
404          * @returns {JXG.Turtle} pointer to the turtle object
405          */
406         setHighlightPenColor: function (color) {
407             //this.visProp.highlightstrokecolor = colStr;
408             this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this.copyAttr('highlightStrokeColor', color));
409             this.objects.push(this.curve);
410             return this;
411         },
412 
413         /**
414          * Sets properties of the turtle, see also {@link JXG.GeometryElement#setAttribute}.
415          * Sets the property for all curves of the turtle in the past and in the future.
416          * @param {Object} attributes key:value pairs
417          * @returns {JXG.Turtle} pointer to the turtle object
418          */
419         setAttribute: function (attributes) {
420             var i, el, tmp,
421                 len = this.objects.length;
422 
423             for (i = 0; i < len; i++) {
424                 el = this.objects[i];
425                 if (el.type === Const.OBJECT_TYPE_CURVE) {
426                     el.setAttribute(attributes);
427                 }
428             }
429 
430             // Set visProp of turtle
431             tmp = this.visProp.id;
432             this.visProp = Type.deepCopy(this.curve.visProp);
433             this.visProp.id = tmp;
434             this._attributes = Type.deepCopy(this.visProp);
435             delete this._attributes.id;
436 
437             return this;
438         },
439 
440         /**
441          * Set a future attribute of the turtle.
442          * @private
443          * @param {String} key
444          * @param {Number|String} val
445          * @returns {Object} pointer to the attributes object
446          */
447         copyAttr: function (key, val) {
448             this._attributes[key.toLowerCase()] = val;
449             return this._attributes;
450         },
451 
452         /**
453          * Sets the visibility of the turtle head to true,
454          * @returns {JXG.Turtle} pointer to the turtle object
455          */
456         showTurtle: function () {
457             this.turtleIsHidden = false;
458             this.arrow.setAttribute({visible: true});
459             this.visProp.arrow.visible = false;
460             this.setPos(this.pos[0], this.pos[1]);
461             this.board.update();
462 
463             return this;
464         },
465 
466         /**
467          * Sets the visibility of the turtle head to false,
468          * @returns {JXG.Turtle} pointer to the turtle object
469          */
470         hideTurtle: function () {
471             this.turtleIsHidden = true;
472             this.arrow.setAttribute({visible: false});
473             this.visProp.arrow.visible = false;
474             this.board.update();
475 
476             return this;
477         },
478 
479         /**
480          * Moves the turtle to position [0,0].
481          * @returns {JXG.Turtle} pointer to the turtle object
482          */
483         home: function () {
484             this.pos = [0, 0];
485             this.setPos(this.pos[0], this.pos[1]);
486 
487             return this;
488         },
489 
490         /**
491          *  Pushes the position of the turtle on the stack.
492          * @returns {JXG.Turtle} pointer to the turtle object
493          */
494         pushTurtle: function () {
495             this.stack.push([this.pos[0], this.pos[1], this.dir]);
496 
497             return this;
498         },
499 
500         /**
501          *  Gets the last position of the turtle on the stack, sets the turtle to this position and removes this
502          * position from the stack.
503          * @returns {JXG.Turtle} pointer to the turtle object
504          */
505         popTurtle: function () {
506             var status = this.stack.pop();
507             this.pos[0] = status[0];
508             this.pos[1] = status[1];
509             this.dir = status[2];
510             this.setPos(this.pos[0], this.pos[1]);
511 
512             return this;
513         },
514 
515         /**
516          * Rotates the turtle into a new direction.
517          * There are two possibilities:
518          * @param {Number|Array} target If a number is given, it is interpreted as the new direction to look to; If an array
519          * consisting of two Numbers is given targeted is used as a pair coordinates.
520          * @returns {JXG.Turtle} pointer to the turtle object
521          */
522         lookTo: function (target) {
523             var ax, ay, bx, by, beta;
524 
525             if (Type.isArray(target)) {
526                 ax = this.pos[0];
527                 ay = this.pos[1];
528                 bx = target[0];
529                 by = target[1];
530 
531                 // Rotate by the slope of the line [this.pos, target]
532                 beta = Math.atan2(by - ay, bx - ax);
533                 this.right(this.dir - (beta * 180 / Math.PI));
534             } else if (Type.isNumber(target)) {
535                 this.right(this.dir - target);
536             }
537             return this;
538         },
539 
540         /**
541          * Moves the turtle to a given coordinate pair.
542          * The direction is not changed.
543          * @param {Array} target Coordinates of the point where the turtle looks to.
544          * @returns {JXG.Turtle} pointer to the turtle object
545          */
546         moveTo: function (target) {
547             var dx, dy, t;
548 
549             if (Type.isArray(target)) {
550                 dx = target[0] - this.pos[0];
551                 dy = target[1] - this.pos[1];
552 
553                 if (!this.turtleIsHidden) {
554                     t = this.board.create('transform', [dx, dy], {type: 'translate'});
555                     t.applyOnce(this.turtle);
556                     t.applyOnce(this.turtle2);
557                 }
558 
559                 if (this.isPenDown) {
560                     // IE workaround
561                     if (this.curve.dataX.length >= 8192) {
562                         this.curve = this.board.create('curve', [[this.pos[0]], [this.pos[1]]], this._attributes);
563                         this.objects.push(this.curve);
564                     }
565                 }
566 
567                 this.pos[0] = target[0];
568                 this.pos[1] = target[1];
569 
570                 if (this.isPenDown) {
571                     this.curve.dataX.push(this.pos[0]);
572                     this.curve.dataY.push(this.pos[1]);
573                 }
574                 this.board.update();
575             }
576 
577             return this;
578         },
579 
580         /**
581          * Alias for {@link JXG.Turtle#forward}
582          */
583         fd: function (len) { return this.forward(len); },
584         /**
585          * Alias for {@link JXG.Turtle#back}
586          */
587         bk: function (len) { return this.back(len); },
588         /**
589          * Alias for {@link JXG.Turtle#left}
590          */
591         lt: function (angle) { return this.left(angle); },
592         /**
593          * Alias for {@link JXG.Turtle#right}
594          */
595         rt: function (angle) { return this.right(angle); },
596         /**
597          * Alias for {@link JXG.Turtle#penUp}
598          */
599         pu: function () { return this.penUp(); },
600         /**
601          * Alias for {@link JXG.Turtle#penDown}
602          */
603         pd: function () { return this.penDown(); },
604         /**
605          * Alias for {@link JXG.Turtle#hideTurtle}
606          */
607         ht: function () { return this.hideTurtle(); },
608         /**
609          * Alias for {@link JXG.Turtle#showTurtle}
610          */
611         st: function () { return this.showTurtle(); },
612         /**
613          * Alias for {@link JXG.Turtle#clearScreen}
614          */
615         cs: function () { return this.clearScreen(); },
616         /**
617          * Alias for {@link JXG.Turtle#pushTurtle}
618          */
619         push: function () { return this.pushTurtle(); },
620         /**
621          * Alias for {@link JXG.Turtle#popTurtle}
622          */
623         pop: function () { return this.popTurtle(); },
624 
625         /**
626          * The "co"-coordinate of the turtle curve at position t is returned.
627          *
628          * @param {Number} t parameter
629          * @param {String} co. Either 'X' or 'Y'.
630          * @returns {Number} x-coordinate of the turtle position or x-coordinate of turtle at position t
631          */
632         evalAt: function (t, co) {
633             var i, j, el, tc,
634                 len = this.objects.length;
635 
636             for (i = 0, j = 0; i < len; i++) {
637                 el = this.objects[i];
638 
639                 if (el.elementClass === Const.OBJECT_CLASS_CURVE) {
640                     if (j <= t && t < j + el.numberPoints) {
641                         tc = (t - j);
642                         return el[co](tc);
643                     }
644                     j += el.numberPoints;
645                 }
646             }
647 
648             return this[co]();
649         },
650 
651         /**
652          * if t is not supplied the x-coordinate of the turtle is returned. Otherwise
653          * the x-coordinate of the turtle curve at position t is returned.
654          * @param {Number} t parameter
655          * @returns {Number} x-coordinate of the turtle position or x-coordinate of turtle at position t
656          */
657         X: function (t) {
658             if (!Type.exists(t)) {
659                 return this.pos[0];
660             }
661 
662             return this.evalAt(t, 'X');
663         },
664 
665         /**
666          * if t is not supplied the y-coordinate of the turtle is returned. Otherwise
667          * the y-coordinate of the turtle curve at position t is returned.
668          * @param {Number} t parameter
669          * @returns {Number} x-coordinate of the turtle position or x-coordinate of turtle at position t
670          */
671         Y: function (t) {
672             if (!Type.exists(t)) {
673                 return this.pos[1];
674             }
675             return this.evalAt(t, 'Y');
676         },
677 
678         /**
679          * @returns {Number} z-coordinate of the turtle position
680          */
681         Z: function (t) {
682             return 1.0;
683         },
684 
685         /**
686          * Gives the lower bound of the parameter if the the turtle is treated as parametric curve.
687          */
688         minX: function () {
689             return 0;
690         },
691 
692         /**
693          * Gives the upper bound of the parameter if the the turtle is treated as parametric curve.
694          * May be overwritten in @see generateTerm.
695          */
696         maxX: function () {
697             var i, el,
698                 len = this.objects.length,
699                 np = 0;
700 
701             for (i = 0; i < len; i++) {
702                 el = this.objects[i];
703                 if (el.elementClass === Const.OBJECT_CLASS_CURVE) {
704                     np += this.objects[i].numberPoints;
705                 }
706             }
707             return np;
708         },
709 
710         /**
711          * Checks whether (x,y) is near the curve.
712          * @param {Number} x Coordinate in x direction, screen coordinates.
713          * @param {Number} y Coordinate in y direction, screen coordinates.
714          * @returns {Boolean} True if (x,y) is near the curve, False otherwise.
715          */
716         hasPoint: function (x, y) {
717             var i, el;
718 
719             // run through all curves of this turtle
720             for (i = 0; i < this.objects.length; i++) {
721                 el = this.objects[i];
722 
723                 if (el.type === Const.OBJECT_TYPE_CURVE) {
724                     if (el.hasPoint(x, y)) {
725                         // So what??? All other curves have to be notified now (for highlighting)
726                         return true;
727                         // This has to be done, yet.
728                     }
729                 }
730             }
731             return false;
732         }
733     });
734 
735     /**
736      * @class This element is used to provide a constructor for a turtle.
737      * @pseudo
738      * @description  Creates a new turtle
739      * @name Turtle
740      * @augments JXG.Turtle
741      * @constructor
742      * @type JXG.Turtle
743      *
744      * @param {JXG.Board} board The board the turtle is put on.
745      * @param {Array} parents
746      * @param {Object} attributes Object containing properties for the element such as stroke-color and visibility. See {@link JXG.GeometryElement#setAttribute}
747      * @returns {JXG.Turtle} Reference to the created turtle object.
748      */
749     JXG.createTurtle = function (board, parents, attributes) {
750         var attr;
751         parents = parents || [];
752 
753         attr = Type.copyAttributes(attributes, board.options, 'turtle');
754         return new JXG.Turtle(board, parents, attr);
755     };
756 
757     JXG.registerElement('turtle', JXG.createTurtle);
758 
759     return {
760         Turtle: JXG.Turtle,
761         createTurtle: JXG.createTurtle
762     };
763 });
764