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);
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         var i, el, len = this.objects.length;
247         for(i=0; i<len; i++) {
248             el = this.objects[i];
249             this.board.removeObject(el);
250         }
251         this.init(0,0, 90);
252         return this;
253     },
254 
255     /**
256     *  Moves the turtle without drawing to a new position
257     * @param {float} x new x- coordinate 
258     * @param {float} y new y- coordinate 
259     * @type {JXG.Turtle}
260     * @return pointer to the turtle object
261     */
262     setPos: function(x,y) {
263         if (JXG.isArray(x)) {
264             this.pos = x;
265         } else {
266             this.pos = [x,y];
267         }
268         if (!this.turtleIsHidden) {
269             this.turtle.setPositionDirectly(JXG.COORDS_BY_USER, [x, y]);
270             this.turtle2.setPositionDirectly(JXG.COORDS_BY_USER, [x, y + this.arrowLen]);
271             var t = this.board.create('transform', 
272                     [-(this.dir-90)*Math.PI/180.0,this.turtle], {type:'rotate'});
273             t.applyOnce(this.turtle2);
274         }
275         this.curve = this.board.create('curve',[[this.pos[0]],[this.pos[1]]], this._attributes);
276         this.objects.push(this.curve);
277         this.board.update();
278         return this;
279     },
280 
281     /**
282     *  Sets the pen size. Equivalent to setProperty({strokeWidth:size})
283     * but affects only the future turtle.
284     * @param {float} size
285     * @type {JXG.Turtle}
286     * @return pointer to the turtle object
287     */
288    setPenSize: function(size) { 
289         //this.visProp.strokewidth = size; 
290         this.curve = this.board.create('curve',[[this.pos[0]],[this.pos[1]]], this.copyAttr('strokeWidth', size));
291         this.objects.push(this.curve);
292         return this;
293     },
294 
295     /**
296     *  Sets the pen color. Equivalent to setProperty({strokeColor:color})
297     * but affects only the future turtle.
298     * @param {string} color
299     * @type {JXG.Turtle}
300     * @return pointer to the turtle object
301     */
302     setPenColor: function(colStr) { 
303         //this.visProp.strokecolor = colStr; 
304         this.curve = this.board.create('curve',[[this.pos[0]],[this.pos[1]]], this.copyAttr('strokeColor', colStr));
305         this.objects.push(this.curve);
306         return this;
307     },
308 
309     /**
310     *  Sets the highlight pen color. Equivalent to setProperty({highlightStrokeColor:color})
311     * but affects only the future turtle.
312     * @param {string} color
313     * @type {JXG.Turtle}
314     * @return pointer to the turtle object
315     */
316     setHighlightPenColor: function(colStr) { 
317         //this.visProp.highlightstrokecolor = colStr; 
318         this.curve = this.board.create('curve',[[this.pos[0]],[this.pos[1]]], this.copyAttr('highlightStrokeColor', colStr));
319         this.objects.push(this.curve);
320         return this;
321     },
322 
323     /**
324     * Sets properties of the turtle, see also {@link JXG.GeometryElement#setProperty}.
325     * Sets the property for all curves of the turtle in the past and in the future.
326     * @param {Object} key:value pairs
327     * @type {JXG.Turtle}
328     * @return pointer to the turtle object
329     */
330     setProperty: function(attributes) {
331         var i, el, len = this.objects.length, tmp;
332         for (i=0; i<len; i++) {
333             el = this.objects[i];
334             if (el.type==JXG.OBJECT_TYPE_CURVE) {
335                 el.setProperty(attributes);
336             }
337         }
338         // Set visProp of turtle
339         tmp = this.visProp['id'];
340         this.visProp = JXG.deepCopy(this.curve.visProp);
341         this.visProp['id'] = tmp;
342         this._attributes = JXG.deepCopy(this.visProp);
343         delete(this._attributes['id']);
344         return this;
345     },
346     
347     /**
348     * Set a future attribute of the turtle.
349     * @private
350     * @param {String} key
351     * @param {Object} value (number, string)
352     * @type {Object}
353     * @return pointer to an attributes object
354     */
355     copyAttr: function(key, val) {
356         this._attributes[key.toLowerCase()] = val;
357         return this._attributes;
358     },
359 
360     /**
361     * Sets the visibility of the turtle head to true,
362     * @type {JXG.Turtle}
363     * @return pointer to the turtle object
364     */
365     showTurtle: function() { 
366         this.turtleIsHidden = false; 
367         this.arrow.setProperty({visible:true});
368         this.visProp.arrow['visible'] = false;
369         this.setPos(this.pos[0],this.pos[1]);
370         this.board.update();
371         return this;
372     },
373 
374     /**
375     * Sets the visibility of the turtle head to false,
376     * @type {JXG.Turtle}
377     * @return pointer to the turtle object
378     */
379     hideTurtle: function() { 
380         this.turtleIsHidden = true;
381         this.arrow.setProperty({visible:false});
382         this.visProp.arrow['visible'] = false;
383         //this.setPos(this.pos[0],this.pos[1]);
384         this.board.update();
385         return this;
386     },
387 
388     /**
389     * Moves the turtle to position [0,0].
390     * @type {JXG.Turtle}
391     * @return pointer to the turtle object
392     */
393     home: function() { 
394         this.pos = [0,0];
395         this.setPos(this.pos[0],this.pos[1]);
396         return this;
397     },
398 
399     /**
400     *  Pushes the position of the turtle on the stack.
401     * @type {JXG.Turtle}
402     * @return pointer to the turtle object
403     */
404     pushTurtle: function() { 
405         this.stack.push([this.pos[0],this.pos[1],this.dir]);
406         return this;
407     },
408 
409     /**
410     *  Gets the last position of the turtle on the stack, sets the turtle to this position and removes this 
411     * position from the stack.
412     * @type {JXG.Turtle}
413     * @return pointer to the turtle object
414     */
415     popTurtle: function() { 
416         var status = this.stack.pop();
417         this.pos[0] = status[0];
418         this.pos[1] = status[1];
419         this.dir = status[2];
420         this.setPos(this.pos[0],this.pos[1]);
421         return this;
422     },
423 
424     /**
425     * Rotates the turtle into a new direction.
426     * There are two possibilities:
427     * @param {float} angle New direction to look to
428     * or
429     * @param {float} x New x coordinate to look to
430     * @param {float} y New y coordinate to look to
431     * @type {JXG.Turtle}
432     * @return pointer to the turtle object
433     */
434     lookTo: function(target) { 
435         if (JXG.isArray(target)) {
436             var ax = this.pos[0];
437             var ay = this.pos[1];
438             var bx = target[0];
439             var by = target[1];
440             var beta; 
441             // Rotate by the slope of the line [this.pos, target]
442             /*
443             var sgn = (bx-ax>0)?1:-1;
444             if (Math.abs(bx-ax)>0.0000001) {
445                 beta = Math.atan2(by-ay,bx-ax)+((sgn<0)?Math.PI:0);  
446             } else {
447                 beta = ((by-ay>0)?0.5:-0.5)*Math.PI;
448             }
449             */
450             beta = Math.atan2(by-ay,bx-ax);
451             this.right(this.dir-(beta*180/Math.PI));
452         } else if (JXG.isNumber(target)) {
453             this.right(this.dir-(target));
454         }
455         return this;
456     },
457 
458     /**
459     * Moves the turtle to a given coordinate pair.
460     * The direction is not changed.
461     * @param {float} x New x coordinate to look to
462     * @param {float} y New y coordinate to look to
463     * @type {JXG.Turtle}
464     * @return pointer to the turtle object
465     */
466     moveTo: function(target) { 
467         if (JXG.isArray(target)) {
468             var dx = target[0]-this.pos[0];
469             var dy = target[1]-this.pos[1];
470             if (!this.turtleIsHidden) {
471                 var t = this.board.create('transform', [dx,dy], {type:'translate'});
472                 t.applyOnce(this.turtle);
473                 t.applyOnce(this.turtle2);
474             }
475             if (this.isPenDown) if (this.curve.dataX.length>=8192) { // IE workaround
476                 this.curve = this.board.create('curve',
477                    [[this.pos[0]],[this.pos[1]]], this._attributes);
478                 this.objects.push(this.curve);
479             }
480             this.pos[0] = target[0];
481             this.pos[1] = target[1];
482             if (this.isPenDown) {
483                 this.curve.dataX.push(this.pos[0]);
484                 this.curve.dataY.push(this.pos[1]);
485             }
486             this.board.update();
487         }
488         return this;
489     },
490 
491     /**
492       * Alias for {@link #forward}
493       */
494     fd: function(len) { return this.forward(len); },
495     /**
496       * Alias for {@link #back}
497       */
498     bk: function(len) { return this.back(len); },
499     /**
500       * Alias for {@link #left}
501       */
502     lt: function(angle) { return this.left(angle); },
503     /**
504       * Alias for {@link #right}
505       */
506     rt: function(angle) { return this.right(angle); },
507     /**
508       * Alias for {@link #penUp}
509       */
510     pu: function() { return this.penUp(); },
511     /**
512       * Alias for {@link #penDown}
513       */
514     pd: function() { return this.penDown(); },
515     /**
516       * Alias for {@link #hideTurtle}
517       */
518     ht: function() { return this.hideTurtle(); },
519     /**
520       * Alias for {@link #showTurtle}
521       */
522     st: function() { return this.showTurtle(); },
523     /**
524       * Alias for {@link #clearScreen}
525       */
526     cs: function() { return this.clearScreen(); },
527     /**
528       * Alias for {@link #pushTurtle}
529       */
530     push: function() { return this.pushTurtle(); },
531     /**
532       * Alias for {@link #popTurtle}
533       */
534     pop: function() { return this.popTurtle(); },
535 
536     /**
537      * the "co"-coordinate of the turtle curve at position t is returned.
538      * @param {float} t parameter 
539      * @param {string} coordinate. Either 'X' or 'Y'.
540      * @return {float} x-coordinate of the turtle position or x-coordinate of turtle at position t
541      */
542     evalAt: function(/** float */ t, /** string */ co) /** float */ { 
543         var i, j, el, tc, len = this.objects.length;
544         for (i=0, j=0; i<len; i++) {
545             el = this.objects[i]; 
546             if (el.elementClass == JXG.OBJECT_CLASS_CURVE) {
547                 if (j<=t && t<j+el.numberPoints) {
548                     tc = (t-j);
549                     return el[co](tc);
550                 }
551                 j += el.numberPoints;
552             }
553         }
554         return this[co]();
555     },
556 
557     /**
558      * if t is not supplied the x-coordinate of the turtle is returned. Otherwise
559      * the x-coordinate of the turtle curve at position t is returned.
560      * @param {float} t parameter 
561      * @return {float} x-coordinate of the turtle position or x-coordinate of turtle at position t
562      */
563     X: function(/** float */ t) /** float */ { 
564         if (typeof t == 'undefined' ) {
565             return this.pos[0]; //this.turtle.X();
566         } else {
567             return this.evalAt(t, 'X');
568         }
569     },
570 
571     /**
572      * if t is not supplied the y-coordinate of the turtle is returned. Otherwise
573      * the y-coordinate of the turtle curve at position t is returned.
574      * @param {float} t parameter 
575      * @return {float} x-coordinate of the turtle position or x-coordinate of turtle at position t
576      */
577     Y: function(/** float */ t) /** float */ { 
578         if (typeof t == 'undefined' ) {
579             return this.pos[1]; //this.turtle.Y();
580         } else {
581             return this.evalAt(t, 'Y');
582         }
583     },
584 
585     /**
586     * @return z-coordinate of the turtle position
587     * @type {float}
588     */
589     Z: function(/** float */ t) /** float */ { 
590         return 1.0; 
591     },
592 
593     /**
594      * Gives the lower bound of the parameter if the the turtle is treated as parametric curve.
595      */
596     minX: function () {
597         return 0;
598     },
599 
600     /**
601      * Gives the upper bound of the parameter if the the turtle is treated as parametric curve.
602      * May be overwritten in @see generateTerm.
603      */
604     maxX: function () {
605         var np = 0, i, len = this.objects.length, el;
606         for (i=0; i <len; i++) {
607             el = this.objects[i];
608             if (el.elementClass == JXG.OBJECT_CLASS_CURVE) {
609                 np += this.objects[i].numberPoints; 
610             }
611         }
612         return np;
613     },
614 
615     /**
616      * Checks whether (x,y) is near the curve.
617      * @param {int} x Coordinate in x direction, screen coordinates.
618      * @param {int} y Coordinate in y direction, screen coordinates.
619      * @param {y} Find closest point on the curve to (x,y)
620      * @return {bool} True if (x,y) is near the curve, False otherwise.
621      */
622     hasPoint: function (x,y) {
623         var i, el;
624         for(i=0;i<this.objects.length;i++) {  // run through all curves of this turtle
625             el = this.objects[i];
626             if (el.type==JXG.OBJECT_TYPE_CURVE) {
627                 if (el.hasPoint(x,y)) {
628                     return true;              // So what??? All other curves have to be notified now (for highlighting)
629                                               // This has to be done, yet.
630                 }
631             }
632         }
633         return false;
634     }
635 });
636 
637 /**
638  * Creates a new turtle
639  * @param {JXG.Board} board The board the turtle is put on.
640  * @param {Array} parents 
641  * @param {Object} attributs Object containing properties for the element such as stroke-color and visibility. See {@link JXG.GeometryElement#setProperty}
642  * @type JXG.Turtle
643  * @return Reference to the created turtle object.
644  */
645 JXG.createTurtle = function(board, parents, attributes) {
646 	var attr;
647     parents = parents || [];
648 
649     attr = JXG.copyAttributes(attributes, board.options, 'turtle');
650     return new JXG.Turtle(board, parents, attr);
651 };
652 
653 JXG.JSXGraph.registerElement('turtle', JXG.createTurtle);
654