PageRenderTime 515ms CodeModel.GetById 121ms app.highlight 229ms RepoModel.GetById 153ms app.codeStats 1ms

/timeplot/scripts/geometry.js

http://showslow.googlecode.com/
JavaScript | 879 lines | 614 code | 105 blank | 160 comment | 167 complexity | d5f605f59fd9c9b404ee028e0ffd8a0d MD5 | raw file
  1/**
  2 * Geometries
  3 * 
  4 * @fileOverview Geometries
  5 * @name Geometries
  6 */
  7
  8/**
  9 * This is the constructor for the default value geometry.
 10 * A value geometry is what regulates mapping of the plot values to the screen y coordinate.
 11 * If two plots share the same value geometry, they will be drawn using the same scale.
 12 * If "min" and "max" parameters are not set, the geometry will stretch itself automatically
 13 * so that the entire plot will be drawn without overflowing. The stretching happens also
 14 * when a geometry is shared between multiple plots, the one with the biggest range will
 15 * win over the others.
 16 * 
 17 * @constructor
 18 */
 19Timeplot.DefaultValueGeometry = function(params) {
 20    if (!params) params = {};
 21    this._id = ("id" in params) ? params.id : "g" + Math.round(Math.random() * 1000000);
 22    this._axisColor = ("axisColor" in params) ? ((typeof params.axisColor == "string") ? new Timeplot.Color(params.axisColor) : params.axisColor) : new Timeplot.Color("#606060"),
 23    this._gridColor = ("gridColor" in params) ? ((typeof params.gridColor == "string") ? new Timeplot.Color(params.gridColor) : params.gridColor) : null,
 24    this._gridLineWidth = ("gridLineWidth" in params) ? params.gridLineWidth : 0.5;
 25    this._axisLabelsPlacement = ("axisLabelsPlacement" in params) ? params.axisLabelsPlacement : "right";
 26    this._gridSpacing = ("gridSpacing" in params) ? params.gridStep : 50;
 27    this._gridType = ("gridType" in params) ? params.gridType : "short";
 28    this._gridShortSize = ("gridShortSize" in params) ? params.gridShortSize : 10;
 29    this._minValue = ("min" in params) ? params.min : null;
 30    this._maxValue = ("max" in params) ? params.max : null;
 31    this._linMap = {
 32        direct: function(v) {
 33            return v;
 34        },
 35        inverse: function(y) {
 36            return y;
 37        }
 38    }
 39    this._map = this._linMap;
 40    this._labels = [];
 41    this._grid = [];
 42}
 43
 44Timeplot.DefaultValueGeometry.prototype = {
 45
 46    /**
 47     * Since geometries can be reused across timeplots, we need to call this function
 48     * before we can paint using this geometry.
 49     */
 50    setTimeplot: function(timeplot) {
 51        this._timeplot = timeplot;
 52        this._canvas = timeplot.getCanvas();
 53        this.reset();
 54    },
 55
 56    /**
 57     * Called by all the plot layers this geometry is associated with
 58     * to update the value range. Unless min/max values are specified
 59     * in the parameters, the biggest value range will be used.
 60     */
 61    setRange: function(range) {
 62        if ((this._minValue == null) || ((this._minValue != null) && (range.min < this._minValue))) {
 63            this._minValue = range.min;
 64        }
 65        if ((this._maxValue == null) || ((this._maxValue != null) && (range.max * 1.05 > this._maxValue))) {
 66            this._maxValue = range.max * 1.05; // get a little more head room to avoid hitting the ceiling
 67        }
 68
 69        this._updateMappedValues();
 70
 71        if (!(this._minValue == 0 && this._maxValue == 0)) {
 72            this._grid = this._calculateGrid();
 73        }
 74    },
 75
 76    /**
 77     * Called after changing ranges or canvas size to reset the grid values
 78     */
 79    reset: function() {
 80        this._clearLabels();
 81        this._updateMappedValues();
 82        this._grid = this._calculateGrid();
 83    },
 84
 85    /**
 86     * Map the given value to a y screen coordinate.
 87     */
 88    toScreen: function(value) {
 89        if (this._canvas && this._maxValue) {
 90            var v = value - this._minValue;
 91            return this._canvas.height * (this._map.direct(v)) / this._mappedRange;
 92        } else {
 93            return -50;
 94        }
 95    },
 96
 97    /**
 98     * Map the given y screen coordinate to a value
 99     */
100    fromScreen: function(y) {
101        if (this._canvas) {
102            return this._map.inverse(this._mappedRange * y / this._canvas.height) + this._minValue;
103        } else {
104            return 0;
105        }
106    },
107
108    /**
109     * Each geometry is also a painter and paints the value grid and grid labels.
110     */
111    paint: function() {
112        if (this._timeplot) {
113            var ctx = this._canvas.getContext('2d');
114    
115            ctx.lineJoin = 'miter';
116    
117            // paint grid
118            if (this._gridColor) {        
119                var gridGradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
120                gridGradient.addColorStop(0, this._gridColor.toHexString());
121                gridGradient.addColorStop(0.3, this._gridColor.toHexString());
122                gridGradient.addColorStop(1, "rgba(255,255,255,0.5)");
123
124                ctx.lineWidth = this._gridLineWidth;
125                ctx.strokeStyle = gridGradient;
126    
127                for (var i = 0; i < this._grid.length; i++) {
128                    var tick = this._grid[i];
129                    var y = Math.floor(tick.y) + 0.5;
130                    if (typeof tick.label != "undefined") {
131                        if (this._axisLabelsPlacement == "left") {
132                            var div = this._timeplot.putText(this._id + "-" + i, tick.label,"timeplot-grid-label",{
133                                left: 4,
134                                bottom: y + 2,
135                                color: this._gridColor.toHexString(),
136                                visibility: "hidden"
137                            });
138                            this._labels.push(div);
139                        } else if (this._axisLabelsPlacement == "right") {
140                            var div = this._timeplot.putText(this._id + "-" + i, tick.label, "timeplot-grid-label",{
141                                right: 4,
142                                bottom: y + 2,
143                                color: this._gridColor.toHexString(),
144                                visibility: "hidden"
145                            });
146                            this._labels.push(div);
147                        }
148                        if (y + div.clientHeight < this._canvas.height + 10) {
149                            div.style.visibility = "visible"; // avoid the labels that would overflow
150                        }
151                    }
152
153                    // draw grid
154                    ctx.beginPath();
155                    if (this._gridType == "long" || tick.label == 0) {
156                        ctx.moveTo(0, y);
157                        ctx.lineTo(this._canvas.width, y);
158                    } else if (this._gridType == "short") {
159                        if (this._axisLabelsPlacement == "left") {
160                            ctx.moveTo(0, y);
161                            ctx.lineTo(this._gridShortSize, y);
162                        } else if (this._axisLabelsPlacement == "right") {
163                            ctx.moveTo(this._canvas.width, y);
164                            ctx.lineTo(this._canvas.width - this._gridShortSize, y);
165                        }                       
166                    }
167                    ctx.stroke();
168                }
169            }
170        
171            // paint axis
172            var axisGradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
173            axisGradient.addColorStop(0, this._axisColor.toString());
174            axisGradient.addColorStop(0.5, this._axisColor.toString());
175            axisGradient.addColorStop(1, "rgba(255,255,255,0.5)");
176            
177            ctx.lineWidth = 1;
178            ctx.strokeStyle = axisGradient;
179    
180            // left axis
181            ctx.beginPath();
182            ctx.moveTo(0,this._canvas.height);
183            ctx.lineTo(0,0);
184            ctx.stroke();
185            
186            // right axis
187            ctx.beginPath();
188            ctx.moveTo(this._canvas.width,0);
189            ctx.lineTo(this._canvas.width,this._canvas.height);
190            ctx.stroke();
191        }
192    },
193    
194    /**
195     * Removes all the labels that were added by this geometry
196     */
197    _clearLabels: function() {
198        for (var i = 0; i < this._labels.length; i++) {
199            var l = this._labels[i];
200            var parent = l.parentNode;
201            if (parent) parent.removeChild(l);
202        }
203    },
204    
205    /*
206     * This function calculates the grid spacing that it will be used 
207     * by this geometry to draw the grid in order to reduce clutter. 
208     */
209    _calculateGrid: function() {
210        var grid = [];
211        
212        if (!this._canvas || this._valueRange == 0) return grid;
213                
214        var power = 0;
215        if (this._valueRange > 1) {
216            while (Math.pow(10,power) < this._valueRange) {
217                power++;
218            }
219            power--;
220        } else {
221            while (Math.pow(10,power) > this._valueRange) {
222                power--;
223            }
224        }
225
226        var unit = Math.pow(10,power);
227        var inc = unit;
228        while (true) {
229            var dy = this.toScreen(this._minValue + inc);
230
231            while (dy < this._gridSpacing) {
232                inc += unit;
233                dy = this.toScreen(this._minValue + inc);
234            }
235
236            if (dy > 2 * this._gridSpacing) { // grids are too spaced out
237                unit /= 10;
238                inc = unit;
239            } else {
240                break;
241            }
242        }
243        
244        var v = 0;
245        var y = this.toScreen(v);
246        if (this._minValue >= 0) {
247            while (y < this._canvas.height) {
248                if (y > 0) {
249                    grid.push({ y: y, label: v });
250                }
251                v += inc;
252                y = this.toScreen(v);
253            }
254        } else if (this._maxValue <= 0) {
255            while (y > 0) {
256                if (y < this._canvas.height) {
257                    grid.push({ y: y, label: v });
258                }
259                v -= inc;
260                y = this.toScreen(v);
261            }
262        } else {
263            while (y < this._canvas.height) {
264                if (y > 0) {
265                    grid.push({ y: y, label: v });
266                }
267                v += inc;
268                y = this.toScreen(v);
269            }
270            v = -inc;
271            y = this.toScreen(v);
272            while (y > 0) {
273                if (y < this._canvas.height) {
274                    grid.push({ y: y, label: v });
275                }
276                v -= inc;
277                y = this.toScreen(v);
278            }
279        }
280        
281        return grid;
282    },
283
284    /*
285     * Update the values that are used by the paint function so that
286     * we don't have to calculate them at every repaint.
287     */
288    _updateMappedValues: function() {
289        this._valueRange = Math.abs(this._maxValue - this._minValue);
290        this._mappedRange = this._map.direct(this._valueRange);
291    }
292    
293}
294
295// --------------------------------------------------
296
297/**
298 * This is the constructor for a Logarithmic value geometry, which
299 * is useful when plots have values in different magnitudes but 
300 * exhibit similar trends and such trends want to be shown on the same
301 * plot (here a cartesian geometry would make the small magnitudes 
302 * disappear).
303 * 
304 * NOTE: this class extends Timeplot.DefaultValueGeometry and inherits
305 * all of the methods of that class. So refer to that class. 
306 * 
307 * @constructor
308 */
309Timeplot.LogarithmicValueGeometry = function(params) {
310    Timeplot.DefaultValueGeometry.apply(this, arguments);
311    this._logMap = {
312        direct: function(v) {
313            return Math.log(v + 1) / Math.log(10);
314        },
315        inverse: function(y) {
316            return Math.exp(Math.log(10) * y) - 1;
317        }
318    }
319    this._mode = "log";
320    this._map = this._logMap;
321    this._calculateGrid = this._logarithmicCalculateGrid;
322};
323
324Timeplot.LogarithmicValueGeometry.prototype._linearCalculateGrid = Timeplot.DefaultValueGeometry.prototype._calculateGrid;
325
326Object.extend(Timeplot.LogarithmicValueGeometry.prototype,Timeplot.DefaultValueGeometry.prototype);
327
328/*
329 * This function calculates the grid spacing that it will be used 
330 * by this geometry to draw the grid in order to reduce clutter. 
331 */
332Timeplot.LogarithmicValueGeometry.prototype._logarithmicCalculateGrid = function() {
333    var grid = [];
334    
335    if (!this._canvas || this._valueRange == 0) return grid;
336
337    var v = 1;
338    var y = this.toScreen(v);
339    while (y < this._canvas.height || isNaN(y)) {
340        if (y > 0) {
341            grid.push({ y: y, label: v });
342        }
343        v *= 10;
344        y = this.toScreen(v);
345    }
346    
347    return grid;
348};
349
350/**
351 * Turn the logarithmic scaling off. 
352 */
353Timeplot.LogarithmicValueGeometry.prototype.actLinear = function() {
354    this._mode = "lin";
355    this._map = this._linMap;
356    this._calculateGrid = this._linearCalculateGrid;
357    this.reset();
358}
359
360/**
361 * Turn the logarithmic scaling on. 
362 */
363Timeplot.LogarithmicValueGeometry.prototype.actLogarithmic = function() {
364    this._mode = "log";
365    this._map = this._logMap;
366    this._calculateGrid = this._logarithmicCalculateGrid;
367    this.reset();
368}
369
370/**
371 * Toggle logarithmic scaling seeting it to on if off and viceversa. 
372 */
373Timeplot.LogarithmicValueGeometry.prototype.toggle = function() {
374    if (this._mode == "log") {
375        this.actLinear();
376    } else {
377        this.actLogarithmic();
378    }
379}
380
381// -----------------------------------------------------
382
383/**
384 * This is the constructor for the default time geometry.
385 * 
386 * @constructor
387 */
388Timeplot.DefaultTimeGeometry = function(params) {
389    if (!params) params = {};
390    this._id = ("id" in params) ? params.id : "g" + Math.round(Math.random() * 1000000);
391    this._locale = ("locale" in params) ? params.locale : "en";
392    this._timeZone = ("timeZone" in params) ? params.timeZone : SimileAjax.DateTime.getTimezone();
393    this._labeler = ("labeller" in params) ? params.labeller : null;
394    this._axisColor = ("axisColor" in params) ? ((params.axisColor == "string") ? new Timeplot.Color(params.axisColor) : params.axisColor) : new Timeplot.Color("#606060"),
395    this._gridColor = ("gridColor" in params) ? ((params.gridColor == "string") ? new Timeplot.Color(params.gridColor) : params.gridColor) : null,
396    this._gridLineWidth = ("gridLineWidth" in params) ? params.gridLineWidth : 0.5;
397    this._axisLabelsPlacement = ("axisLabelsPlacement" in params) ? params.axisLabelsPlacement : "bottom";
398    this._gridStep = ("gridStep" in params) ? params.gridStep : 100;
399    this._gridStepRange = ("gridStepRange" in params) ? params.gridStepRange : 20;
400    this._min = ("min" in params) ? params.min : null;
401    this._max = ("max" in params) ? params.max : null;
402    this._timeValuePosition =("timeValuePosition" in params) ? params.timeValuePosition : "bottom";
403    this._unit = ("unit" in params) ? params.unit : SimileAjax.NativeDateUnit;
404    this._linMap = {
405        direct: function(t) {
406            return t;
407        },
408        inverse: function(x) {
409            return x;
410        }
411    }
412    this._map = this._linMap;
413    if (!this._labeler)
414        this._labeler = (this._unit && ("createLabeller" in this._unit)) ? this._unit.createLabeller(this._locale, this._timeZone) : new Timeline.GregorianDateLabeller(this._locale, this._timeZone);
415    var dateParser = this._unit.getParser("iso8601");
416    if (this._min && !this._min.getTime) {
417        this._min = dateParser(this._min);
418    }
419    if (this._max && !this._max.getTime) {
420        this._max = dateParser(this._max);
421    }
422    this._labels = [];
423    this._grid = [];
424}
425
426Timeplot.DefaultTimeGeometry.prototype = {
427
428    /**
429     * Since geometries can be reused across timeplots, we need to call this function
430     * before we can paint using this geometry.
431     */
432    setTimeplot: function(timeplot) {
433        this._timeplot = timeplot;
434        this._canvas = timeplot.getCanvas();
435        this.reset();
436    },
437
438    /**
439     * Called by all the plot layers this geometry is associated with
440     * to update the time range. Unless min/max values are specified
441     * in the parameters, the biggest range will be used.
442     */
443    setRange: function(range) {
444        if (this._min) {
445            this._earliestDate = this._min;
446        } else if (range.earliestDate && ((this._earliestDate == null) || ((this._earliestDate != null) && (range.earliestDate.getTime() < this._earliestDate.getTime())))) {
447            this._earliestDate = range.earliestDate;
448        }
449        
450        if (this._max) {
451            this._latestDate = this._max;
452        } else if (range.latestDate && ((this._latestDate == null) || ((this._latestDate != null) && (range.latestDate.getTime() > this._latestDate.getTime())))) {
453            this._latestDate = range.latestDate;
454        }
455
456        if (!this._earliestDate && !this._latestDate) {
457            this._grid = [];
458        } else {
459            this.reset(); 
460        }
461    },
462    
463    /**
464     * Called after changing ranges or canvas size to reset the grid values
465     */
466    reset: function() {
467        this._updateMappedValues();
468        if (this._canvas) this._grid = this._calculateGrid();
469    },
470    
471    /**
472     * Map the given date to a x screen coordinate.
473     */
474    toScreen: function(time) {
475        if (this._canvas && this._latestDate) {
476            var t = time - this._earliestDate.getTime();
477            var fraction = (this._mappedPeriod > 0) ? this._map.direct(t) / this._mappedPeriod : 0;
478            return this._canvas.width * fraction;
479        } else {
480            return -50;
481        } 
482    },
483
484    /**
485     * Map the given x screen coordinate to a date.
486     */
487    fromScreen: function(x) {
488        if (this._canvas) {
489            return this._map.inverse(this._mappedPeriod * x / this._canvas.width) + this._earliestDate.getTime();
490        } else {
491            return 0;
492        } 
493    },
494    
495    /**
496     * Get a period (in milliseconds) this time geometry spans.
497     */
498    getPeriod: function() {
499        return this._period;
500    },
501    
502    /**
503     * Return the labeler that has been associated with this time geometry
504     */
505    getLabeler: function() {
506        return this._labeler;
507    },
508
509    /**
510     * Return the time unit associated with this time geometry
511     */
512    getUnit: function() {
513        return this._unit;
514    },
515
516   /**
517    * Each geometry is also a painter and paints the value grid and grid labels.
518    */
519    paint: function() {
520        if (this._canvas) {
521            var unit = this._unit;
522            var ctx = this._canvas.getContext('2d');
523    
524            var gradient = ctx.createLinearGradient(0,0,0,this._canvas.height);
525    
526            ctx.strokeStyle = gradient;
527            ctx.lineWidth = this._gridLineWidth;
528            ctx.lineJoin = 'miter';
529    
530            // paint grid
531            if (this._gridColor) {        
532                gradient.addColorStop(0, this._gridColor.toString());
533                gradient.addColorStop(1, "rgba(255,255,255,0.9)");
534    
535                for (var i = 0; i < this._grid.length; i++) {
536                    var tick = this._grid[i];
537                    var x = Math.floor(tick.x) + 0.5;
538                    if (this._axisLabelsPlacement == "top") {
539                        var div = this._timeplot.putText(this._id + "-" + i, tick.label,"timeplot-grid-label",{
540                            left: x + 4,
541                            top: 2,
542                            visibility: "hidden"
543                        });
544                        this._labels.push(div);
545                    } else if (this._axisLabelsPlacement == "bottom") {
546                        var div = this._timeplot.putText(this._id + "-" + i, tick.label, "timeplot-grid-label",{
547                            left: x + 4,
548                            bottom: 2,
549                            visibility: "hidden"
550                        });
551                        this._labels.push(div);
552                    }
553                    if (x + div.clientWidth < this._canvas.width + 10) {
554                        div.style.visibility = "visible"; // avoid the labels that would overflow
555                    }
556
557                    // draw separator
558                    ctx.beginPath();
559                    ctx.moveTo(x,0);
560                    ctx.lineTo(x,this._canvas.height);
561                    ctx.stroke();
562                }
563            }
564    
565            // paint axis
566            gradient.addColorStop(0, this._axisColor.toString());
567            gradient.addColorStop(1, "rgba(255,255,255,0.5)");
568            
569            ctx.lineWidth = 1;
570            gradient.addColorStop(0, this._axisColor.toString());
571    
572            ctx.beginPath();
573            ctx.moveTo(0,0);
574            ctx.lineTo(this._canvas.width,0);
575            ctx.stroke();
576        }
577    },
578    
579    /*
580     * This function calculates the grid spacing that it will be used 
581     * by this geometry to draw the grid in order to reduce clutter. 
582     */
583    _calculateGrid: function() {
584        var grid = [];
585        
586        var time = SimileAjax.DateTime;
587        var u = this._unit;
588        var p = this._period;
589        
590        if (p == 0) return grid;
591        
592        // find the time units nearest to the time period
593        if (p > time.gregorianUnitLengths[time.MILLENNIUM]) {
594            unit = time.MILLENNIUM; 
595        } else {
596            for (var unit = time.MILLENNIUM; unit > 0; unit--) {
597                if (time.gregorianUnitLengths[unit-1] <= p && p < time.gregorianUnitLengths[unit]) {
598                    unit--;
599                    break;
600                }
601            }
602        }
603
604        var t = u.cloneValue(this._earliestDate);
605
606        do {
607            time.roundDownToInterval(t, unit, this._timeZone, 1, 0);
608            var x = this.toScreen(u.toNumber(t));
609            switch (unit) {
610                case time.SECOND:
611                  var l = t.toLocaleTimeString();
612                  break;
613                case time.MINUTE:
614                  var m = t.getMinutes();
615                  var l = t.getHours() + ":" + ((m < 10) ? "0" : "") + m;
616                  break;
617                case time.HOUR:
618                  var l = t.getHours() + ":00";
619                  break;
620                case time.DAY:
621                case time.WEEK:
622                case time.MONTH:
623                  var l = t.toLocaleDateString();
624                  break;  
625                case time.YEAR:
626                case time.DECADE:
627                case time.CENTURY:
628                case time.MILLENNIUM:
629                  var l = t.getUTCFullYear();
630                  break;
631            }
632            if (x > 0) { 
633                grid.push({ x: x, label: l });
634            }
635            time.incrementByInterval(t, unit, this._timeZone);
636        } while (t.getTime() < this._latestDate.getTime());
637        
638        return grid;
639    },
640
641    /*
642     * Clear labels generated by this time geometry.
643     */
644    _clearLabels: function() {
645        for (var i = 0; i < this._labels.length; i++) {
646            var l = this._labels[i];
647            var parent = l.parentNode;
648            if (parent) parent.removeChild(l);
649        }
650    },
651        
652    /*
653     * Update the values that are used by the paint function so that
654     * we don't have to calculate them at every repaint.
655     */
656    _updateMappedValues: function() {
657        if (this._latestDate && this._earliestDate) {
658            this._period = this._latestDate.getTime() - this._earliestDate.getTime();
659            this._mappedPeriod = this._map.direct(this._period);
660        } else {
661            this._period = 0;
662            this._mappedPeriod = 0;
663        }
664    }
665    
666}
667
668// --------------------------------------------------------------
669
670/**
671 * This is the constructor for the magnifying time geometry.
672 * Users can interact with this geometry and 'magnify' certain areas of the
673 * plot to see the plot enlarged and resolve details that would otherwise
674 * get lost or cluttered with a linear time geometry.
675 * 
676 * @constructor
677 */
678Timeplot.MagnifyingTimeGeometry = function(params) {
679    Timeplot.DefaultTimeGeometry.apply(this, arguments);
680        
681    var g = this;
682    this._MagnifyingMap = {
683        direct: function(t) {
684            if (t < g._leftTimeMargin) {
685                var x = t * g._leftRate;
686            } else if ( g._leftTimeMargin < t && t < g._rightTimeMargin ) {
687                var x = t * g._expandedRate + g._expandedTimeTranslation;
688            } else {
689                var x = t * g._rightRate + g._rightTimeTranslation;
690            }
691            return x;
692        },
693        inverse: function(x) {
694            if (x < g._leftScreenMargin) {
695                var t = x / g._leftRate;
696            } else if ( g._leftScreenMargin < x && x < g._rightScreenMargin ) {
697                var t = x / g._expandedRate + g._expandedScreenTranslation;
698            } else {
699                var t = x / g._rightRate + g._rightScreenTranslation;
700            }
701            return t;
702        }
703    }
704
705    this._mode = "lin";
706    this._map = this._linMap;
707};
708
709Object.extend(Timeplot.MagnifyingTimeGeometry.prototype,Timeplot.DefaultTimeGeometry.prototype);
710
711/**
712 * Initialize this geometry associating it with the given timeplot and 
713 * register the geometry event handlers to the timeplot so that it can
714 * interact with the user.
715 */
716Timeplot.MagnifyingTimeGeometry.prototype.initialize = function(timeplot) {
717    Timeplot.DefaultTimeGeometry.prototype.initialize.apply(this, arguments);
718
719    if (!this._lens) {
720        this._lens = this._timeplot.putDiv("lens","timeplot-lens");
721    }
722
723    var period = 1000 * 60 * 60 * 24 * 30; // a month in the magnifying lens
724
725    var geometry = this;
726    
727    var magnifyWith = function(lens) {
728        var aperture = lens.clientWidth;
729        var loc = geometry._timeplot.locate(lens);
730        geometry.setMagnifyingParams(loc.x + aperture / 2, aperture, period);
731        geometry.actMagnifying();
732        geometry._timeplot.paint();
733    }
734    
735    var canvasMouseDown = function(elmt, evt, target) {
736        geometry._canvas.startCoords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
737        geometry._canvas.pressed = true;
738    }
739    
740    var canvasMouseUp = function(elmt, evt, target) {
741        geometry._canvas.pressed = false;
742        var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
743        if (Timeplot.Math.isClose(coords,geometry._canvas.startCoords,5)) {
744            geometry._lens.style.display = "none";
745            geometry.actLinear();
746            geometry._timeplot.paint();
747        } else {
748            geometry._lens.style.cursor = "move";
749            magnifyWith(geometry._lens);
750        }
751    }
752
753    var canvasMouseMove = function(elmt, evt, target) {
754        if (geometry._canvas.pressed) {
755            var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
756            if (coords.x < 0) coords.x = 0;
757            if (coords.x > geometry._canvas.width) coords.x = geometry._canvas.width;
758            geometry._timeplot.placeDiv(geometry._lens, {
759                left: geometry._canvas.startCoords.x,
760                width: coords.x - geometry._canvas.startCoords.x,
761                bottom: 0,
762                height: geometry._canvas.height,
763                display: "block"
764            });
765        }
766    }
767
768    var lensMouseDown = function(elmt, evt, target) {
769        geometry._lens.startCoords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);;
770        geometry._lens.pressed = true; 
771    }
772    
773    var lensMouseUp = function(elmt, evt, target) {
774        geometry._lens.pressed = false;
775    }
776    
777    var lensMouseMove = function(elmt, evt, target) {
778        if (geometry._lens.pressed) {
779            var coords = SimileAjax.DOM.getEventRelativeCoordinates(evt,elmt);
780            var lens = geometry._lens;
781            var left = lens.offsetLeft + coords.x - lens.startCoords.x;
782            if (left < geometry._timeplot._paddingX) left = geometry._timeplot._paddingX;
783            if (left + lens.clientWidth > geometry._canvas.width - geometry._timeplot._paddingX) left = geometry._canvas.width - lens.clientWidth + geometry._timeplot._paddingX;
784            lens.style.left = left;
785            magnifyWith(lens);
786        }
787    }
788    
789    if (!this._canvas.instrumented) {
790        SimileAjax.DOM.registerEvent(this._canvas, "mousedown", canvasMouseDown);
791        SimileAjax.DOM.registerEvent(this._canvas, "mousemove", canvasMouseMove);
792        SimileAjax.DOM.registerEvent(this._canvas, "mouseup"  , canvasMouseUp);
793        SimileAjax.DOM.registerEvent(this._canvas, "mouseup"  , lensMouseUp);
794        this._canvas.instrumented = true;
795    }
796    
797    if (!this._lens.instrumented) {
798        SimileAjax.DOM.registerEvent(this._lens, "mousedown", lensMouseDown);
799        SimileAjax.DOM.registerEvent(this._lens, "mousemove", lensMouseMove);
800        SimileAjax.DOM.registerEvent(this._lens, "mouseup"  , lensMouseUp);
801        SimileAjax.DOM.registerEvent(this._lens, "mouseup"  , canvasMouseUp);
802        this._lens.instrumented = true;
803    }
804}
805
806/**
807 * Set the Magnifying parameters. c is the location in pixels where the Magnifying
808 * center should be located in the timeplot, a is the aperture in pixel of
809 * the Magnifying and b is the time period in milliseconds that the Magnifying 
810 * should span.
811 */
812Timeplot.MagnifyingTimeGeometry.prototype.setMagnifyingParams = function(c,a,b) {
813    a = a / 2;
814    b = b / 2;
815
816    var w = this._canvas.width;
817    var d = this._period;
818
819    if (c < 0) c = 0;
820    if (c > w) c = w;
821    
822    if (c - a < 0) a = c;
823    if (c + a > w) a = w - c;
824    
825    var ct = this.fromScreen(c) - this._earliestDate.getTime();
826    if (ct - b < 0) b = ct;
827    if (ct + b > d) b = d - ct;
828
829    this._centerX = c;
830    this._centerTime = ct;
831    this._aperture = a;
832    this._aperturePeriod = b;
833    
834    this._leftScreenMargin = this._centerX - this._aperture;
835    this._rightScreenMargin = this._centerX + this._aperture;
836    this._leftTimeMargin = this._centerTime - this._aperturePeriod;
837    this._rightTimeMargin = this._centerTime + this._aperturePeriod;
838        
839    this._leftRate = (c - a) / (ct - b);
840    this._expandedRate = a / b;
841    this._rightRate = (w - c - a) / (d - ct - b);
842
843    this._expandedTimeTranslation = this._centerX - this._centerTime * this._expandedRate; 
844    this._expandedScreenTranslation = this._centerTime - this._centerX / this._expandedRate;
845    this._rightTimeTranslation = (c + a) - (ct + b) * this._rightRate;
846    this._rightScreenTranslation = (ct + b) - (c + a) / this._rightRate;
847
848    this._updateMappedValues();
849}
850
851/*
852 * Turn magnification off.
853 */
854Timeplot.MagnifyingTimeGeometry.prototype.actLinear = function() {
855    this._mode = "lin";
856    this._map = this._linMap;
857    this.reset();
858}
859
860/*
861 * Turn magnification on.
862 */
863Timeplot.MagnifyingTimeGeometry.prototype.actMagnifying = function() {
864    this._mode = "Magnifying";
865    this._map = this._MagnifyingMap;
866    this.reset();
867}
868
869/*
870 * Toggle magnification.
871 */
872Timeplot.MagnifyingTimeGeometry.prototype.toggle = function() {
873    if (this._mode == "Magnifying") {
874        this.actLinear();
875    } else {
876        this.actMagnifying();
877    }
878}
879