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