static/tiles2.js
changeset 45 0ce4064c428e
parent 43 fcd818eb5a71
child 46 82d7b4d64cc6
--- a/static/tiles2.js	Thu Jan 07 22:25:05 2010 +0200
+++ b/static/tiles2.js	Sat Jan 09 13:26:40 2010 +0200
@@ -1,4 +1,18 @@
 // A source of tile images of a specific width/height, zoom level range, and some other attributes
+
+/**
+ * A source of image tiles.
+ *
+ * The image source is expected to serve fixed-size tiles (tile_width × tile_height) of image data based on the 
+ * x, y, zl URL query parameters. 
+ *
+ *  x, y        - the pixel coordinates of the top-left corner
+ *                XXX: these are scaled from the image coordinates by the zoom level
+ *
+ *  zl          - the zoom level used, out < zl < in.
+ *                The image pixels are scaled by powers-of-two, so a 256x256 tile at zl=-1 shows a 512x512 area of the
+ *                1:1 image.
+ */
 var Source = Class.create({
     initialize: function (path, tile_width, tile_height, zoom_min, zoom_max, img_width, img_height) {
         this.path = path;
@@ -13,26 +27,34 @@
         this.opt_key = this.opt_value = null;
     },
     
-    // build a URL for the given tile image
-    build_url: function (col, row, zl, sw, sh) {
-        // two-bit hash (0-4) based on the (col, row)
-        var hash = ( (col % 2) << 1 | (row % 2) ) + 1;
-        
-        // the subdomain to use
-        var subdomain = "";
-        
-        if (0)
+    /**
+     * Return an URL representing the tile at the given viewport (row, col) at the given zl.
+     *
+     * XXX: sw/sh = screen-width/height, used to choose an appropriate output size for dynamic image sources...
+     */
+    build_url: function (col, row, zl /*, sw, sh */) {
+        // XXX: distribute tile requests across tile*.foo.bar
+        if (0) {
+            // two-bit hash (0-4) based on the (col, row)
+            var hash = ( (col % 2) << 1 | (row % 2) ) + 1;
+            
+            // the subdomain to use
+            var subdomain = "";
+            
             subdomain = "tile" + hash + ".";
+        }
 
         // the (x, y) co-ordinates
         var x = col * this.tile_width;
         var y = row * this.tile_height;
 
         var url = this.path + "?x=" + x + "&y=" + y + "&zl=" + zl; // + "&sw=" + sw + "&sh=" + sh;
-
+        
+        // refresh the tile each time it is loaded?
         if (this.refresh)
             url += "&ts=" + new Date().getTime();
-
+        
+        // XXX: additional parameters, not used
         if (this.opt_key && this.opt_value)
             url += "&" + this.opt_key + "=" + this.opt_value;
 
@@ -45,25 +67,21 @@
     }
 });
 
-// a viewport that contains a substrate which contains several zoom layers which contain many tiles
+/**
+ * Viewport implements the tiles-UI. It consists of a draggable substrate, which in turn consists of several
+ * ZoomLayers, which then contain the actual tile images.
+ *
+ * Vars:
+ *      scroll_x/y      - the visible pixel offset of the top-left corner
+ */
 var Viewport = Class.create({
     initialize: function (source, viewport_id) {
         this.source = source;
-
+        
+        // get a handle on the UI elements
         this.id = viewport_id;
         this.div = $(viewport_id);
         this.substrate = this.div.down("div.substrate");
-    
-        // the stack of zoom levels
-        this.zoom_layers = [];
-        
-        // pre-populate the stack
-        for (var zoom_level = source.zoom_min; zoom_level <= source.zoom_max; zoom_level++) {
-            var zoom_layer = new ZoomLayer(source, zoom_level);
-
-            this.substrate.appendChild(zoom_layer.div);
-            this.zoom_layers[zoom_level] = zoom_layer;
-        }
 
         // make the substrate draggable
         this.draggable = new Draggable(this.substrate, {
@@ -72,13 +90,13 @@
             onEnd: this.on_scroll_end.bind(this),
         });
 
-        // event handlers
+        // register event handlers for other UI functions
         Event.observe(this.substrate, "dblclick", this.on_dblclick.bindAsEventListener(this));
         Event.observe(this.substrate, "mousewheel", this.on_mousewheel.bindAsEventListener(this));
         Event.observe(this.substrate, "DOMMouseScroll", this.on_mousewheel.bindAsEventListener(this));     // mozilla
         Event.observe(document, "resize", this.on_resize.bindAsEventListener(this));
         
-        // zoom buttons
+        // init zoom UI buttons
         this.btn_zoom_in = $("btn-zoom-in");
         this.btn_zoom_out = $("btn-zoom-out");
 
@@ -87,57 +105,102 @@
         
         if (this.btn_zoom_out)
             Event.observe(this.btn_zoom_out, "click", this.zoom_out.bindAsEventListener(this));
-    
-        // set viewport size
-        this.update_size();
+           
+        // initial view location (centered)
+        var cx = 0, cy = 0, zl = 0;
         
-        // this comes after update_size, since it must be updated once we have the size and zoom layer...
-        this.image_link = $("lnk-image");
-            
-        // initial location
-        var cx = 0, cy = 0, z = 0;
-        
-        // initial location?    
+        // from link?
         if (document.location.hash) {
-            // x:y:z tuple
+            // parse x:y:z tuple
             var pt = document.location.hash.substr(1).split(":");
             
             // unpack    
             if (pt.length) cx = parseInt(pt.shift());
             if (pt.length) cy = parseInt(pt.shift());
-            if (pt.length) z = parseInt(pt.shift());
+            if (pt.length) zl = parseInt(pt.shift());
 
         } else {
             // start in the center
             cx = this.source.img_width / 2;
             cy = this.source.img_height / 2;
-            z = 0; // XXX: xy unscaled: (this.source.zoom_min + this.source.zoom_max) / 2;
+            zl = 0; // XXX: need to scale x/y for this: (this.source.zoom_min + this.source.zoom_max) / 2;
         }
 
-        // initial view
-        this.zoom_scaled(
-            cx - this.center_offset_x, 
-            cy - this.center_offset_y, 
-            z
-        );
+        // initialize zoom state to given zl
+        this._init_zoom(zl);
+        
+        // initialize viewport size
+        this.update_size();
+        
+        // initialize scroll offset
+        this._init_scroll(cx, cy);
+        
+        // this comes after update_size, so that the initial update_size doesn't try and update the image_link,
+        // since this only works once we have the zoom layers set up...
+        this.image_link = $("lnk-image");
+        this.update_image_link();
+
+        // display tiles!
+        this.update_tiles();
     },
+
+/*
+ * Initializers - only run once
+ */
+                    
+    /** Initialize the zoom state to show the given zoom level */
+    _init_zoom: function (zl) {
+        // the stack of zoom levels
+        this.zoom_layers = [];
+        
+        // populate the zoom-layers stack based on the number of zoom levels defined for the source
+        for (var zoom_level = this.source.zoom_min; zoom_level <= this.source.zoom_max; zoom_level++) {
+            var zoom_layer = new ZoomLayer(this.source, zoom_level);
+
+            this.substrate.appendChild(zoom_layer.div);
+            this.zoom_layers[zoom_level] = zoom_layer;
+        }
+
+        // is the new zoom level valid?
+        if (!this.zoom_layers[zl])
+            // XXX: nasty, revert to something else?
+            return false;
+        
+        // set the zoom layyer
+        this.zoom_layer = this.zoom_layers[zl];
+        
+        // enable it with initial z-index
+        this.zoom_layer.enable(11);
+        
+        // init the UI accordingly
+        this.update_zoom_ui();
+    },
+
+    /** Initialize the scroll state to show the given (scaled) centered coordinates */
+    _init_scroll: function (cx, cy) {
+        // scroll center
+        this.scroll_center_to(cx, cy);
+    },
+
     
-    /* event handlers */
+/*
+ * Handle input events
+ */    
 
-    // window resized
+    /** Viewport resized */
     on_resize: function (ev) {
         this.update_size();
         this.update_tiles();
     },
     
-    // double-click handler
+    /** Double-click to zoom and center */
     on_dblclick: function (ev) {
         var offset = this.event_offset(ev);
         
-        this.zoom_center_to(
+        // center view and zoom in
+        this.center_and_zoom_in(
             this.scroll_x + offset.x,
-            this.scroll_y + offset.y,
-            1   // zoom in
+            this.scroll_y + offset.y
         );
     },
 
@@ -185,25 +248,23 @@
         this.zoom_center_to(x, y, delta);
     },
     
-    // substrate scroll was started
+    /** Substrate scroll was started */
     on_scroll_start: function (ev) {
 
     },
     
-    // substrate was scrolled, update scroll_{x,y}, and then update tiles after 100ms
+    /** Substrate was scrolled, update scroll_{x,y}, and then update tiles after 100ms */
     on_scroll_move: function (ev) {
         this.update_scroll();
         this.update_after_timeout();
     },
     
-    // substrate scroll was ended, update tiles now
+    /** Substrate scroll was ended, update tiles now */
     on_scroll_end: function (ev) {
         this.update_now();
     },
 
-    /* get state */
-
-    // return the absolute (x, y) coords of the given event inside the viewport
+    /** Calculate the absolute (x, y) coords of the given event inside the viewport */
     event_offset: function (ev) {
         var offset = this.div.cumulativeOffset();
 
@@ -213,9 +274,11 @@
         };
     },
 
-    /* modify state */
+/*
+ * Change view - scroll/zoom
+ */                  
 
-    // scroll the view to place the given absolute (x, y) co-ordinate at the top left
+    /** Scroll the view to place the given absolute (x, y) co-ordinate at the top left */
     scroll_to: function (x, y) {
         // update it via the style
         this.substrate.style.top = "-" + y + "px";
@@ -226,7 +289,7 @@
         this.scroll_y = y;
     },
 
-    // scroll the view to place the given absolute (x, y) co-ordinate at the center
+    /** Scroll the view to place the given absolute (x, y) co-ordinate at the center */
     scroll_center_to: function (x, y) {
         return this.scroll_to(
             x - this.center_offset_x,
@@ -234,22 +297,22 @@
         );
     },
  
-    // zoom à la delta such that the given (zoomed) absolute (x, y) co-ordinates will be at the top left
+    /** Zoom à la delta such that the given (zoomed) absolute (x, y) co-ordinates will be at the top left */
     zoom_scaled: function (x, y, delta) {
         if (!this.update_zoom(delta))
+            // couldn't zoom, scaled coords are wrong
             return false;
 
         // scroll to the new position
         this.scroll_to(x, y);
         
-        // update view
-        // XXX: ...
+        // update view after 100ms - in case we zoom again?
         this.update_after_timeout();
         
         return true;
     },
    
-    // zoom à la delta such that the given (current) absolute (x, y) co-ordinates will be at the top left
+    /** Zoom à la delta such that the given (current) absolute (x, y) co-ordinates will be at the top left */
     zoom_to: function (x, y, delta) {
         return this.zoom_scaled(
             scaleByZoomDelta(x, delta),
@@ -258,7 +321,7 @@
         );
     },
     
-    // zoom à la delta such that the given (current) absolute (x, y) co-ordinates will be at the center
+    /** Zoom à la delta such that the given (current) absolute (x, y) co-ordinates will be at the center */
     zoom_center_to: function (x, y, delta) {
         return this.zoom_scaled(
             scaleByZoomDelta(x, delta) - this.center_offset_x,
@@ -267,7 +330,7 @@
         );
     },
 
-    // zoom à la delta, keeping the view centered
+    /** Zoom à la delta, keeping the view centered */
     zoom_centered: function (delta) {
         return this.zoom_center_to(
             this.scroll_x + this.center_offset_x,
@@ -276,105 +339,137 @@
         );
     },
 
-    // zoom in one level, keeping the view centered
+    /** Zoom in one level, keeping the view centered */
     zoom_in: function () {
         return this.zoom_centered(+1);
     },
     
-    // zoom out one leve, keeping the view centered
+    /** Zoom out one level, keeping the view centered */
     zoom_out: function () {
         return this.zoom_centered(-1);
     },
 
-    /* update view/state to reflect reality */
+    /** Center the view on the given coords, and zoom in, if possible */
+    center_and_zoom_in: function (cx, cy) {
+        // try and zoom in
+        if (this.update_zoom(1)) {
+            // scaled coords
+            cx = scaleByZoomDelta(cx, 1);
+            cy = scaleByZoomDelta(cy, 1);
+        }
 
-    // figure out the viewport dimensions
+        // re-center
+        this.scroll_center_to(cx, cy);
+        
+        // update view after 100ms - in case we zoom again?
+        this.update_after_timeout();
+    },
+
+/*
+ * Update view state
+ */              
+ 
+    /** Update the view_* / center_offset_* vars, and any dependent items */
     update_size: function () {
         this.view_width = this.div.getWidth();
         this.view_height = this.div.getHeight();
 
         this.center_offset_x = Math.floor(this.view_width / 2);
         this.center_offset_y = Math.floor(this.view_height / 2);
-
+        
+        // the link-to-image uses the current view size
         this.update_image_link();
     },
-    
-    // figure out the scroll offset as absolute pixel co-ordinates at the top left
+   
+    /**
+     * Update the scroll_x/y state
+     */    
     update_scroll: function() {
         this.scroll_x = -parseInt(this.substrate.style.left);
         this.scroll_y = -parseInt(this.substrate.style.top);
     },
-
-    // wiggle the ZoomLevels around to match the current zoom level
-    update_zoom: function(delta) {
-        if (!this.zoom_layer) {
-            // first zoom operation
-
-            // is the new zoom level valid?
-            if (!this.zoom_layers[delta])
-                return false;
-            
-            // set the zoom layyer
-            this.zoom_layer = this.zoom_layers[delta];
-            
-            // enable it
-            this.zoom_layer.enable(11);
-            
-            // no need to .update_tiles or anything like that
-            
-        } else {
-            // is the new zoom level valid?
-            if (!this.zoom_layers[this.zoom_layer.level + delta])
-                return false;
+    
+    /**
+     * Switch zoom layers
+     */
+    update_zoom: function (delta) {
+        // is the new zoom level valid?
+        if (!this.zoom_layers[this.zoom_layer.level + delta])
+            return false;
 
-            var zoom_old = this.zoom_layer;
-            var zoom_new = this.zoom_layers[this.zoom_layer.level + delta];
-            
-            // XXX: ugly hack
-            if (this.zoom_timer) {
-                clearTimeout(this.zoom_timer);
-                this.zoom_timer = null;
-            }
-            
-            // get other zoom layers out of the way
-            this.zoom_layers.each(function (zl) {
-                zl.disable();
-            });
-            
-            // update the zoom layer
-            this.zoom_layer = zoom_new;
-            
-            // apply new z-indexes, preferr the current one over the new one
-            zoom_new.enable(11);
-            zoom_old.enable(10);
-            
-            // resize the tiles in the two zoom layers
-            zoom_new.update_tiles(zoom_new.level);
-            zoom_old.update_tiles(zoom_old.level);
-            
-            // XXX: ugly hack
-            this.zoom_timer = setTimeout(function () { zoom_old.disable()}, 1000);
+        var zoom_old = this.zoom_layer;
+        var zoom_new = this.zoom_layers[this.zoom_layer.level + delta];
+        
+        // XXX: clear hide-zoom-after-timeout
+        if (this.zoom_timer) {
+            clearTimeout(this.zoom_timer);
+            this.zoom_timer = null;
         }
+        
+        // get other zoom layers out of the way
+        // XXX: u
+        this.zoom_layers.each(function (zl) {
+            zl.disable();
+        });
+        
+        // update the current zoom layer
+        this.zoom_layer = zoom_new;
+        
+        // layer them such that the old on remains visible underneath the new one
+        zoom_new.enable(11);
+        zoom_old.enable(10);
+        
+        // resize the tiles in the two zoom layers
+        zoom_new.update_tiles(zoom_new.level);
+        zoom_old.update_tiles(zoom_new.level);
+        
+        // disable the old zoom layer after 1000ms - after the new zoom layer has loaded - not an optimal solution
+        this.zoom_timer = setTimeout(function () { zoom_old.disable()}, 1000);
 
-        // update UI
+        // update UI state
         this.update_zoom_ui();
 
         return true;
     },
 
-    // keep the zoom buttons, if any, updated
-    update_zoom_ui: function () {
-        if (this.btn_zoom_in)
-            (this.zoom_layer.level >= this.source.zoom_max) ? this.btn_zoom_in.disable() : this.btn_zoom_in.enable();
+    /** Schedule an update_tiles() after a 100ms interval */
+    update_after_timeout: function () {
+        // have not called update_tiles() yet
+        this._update_idle = false;
         
-        if (this.btn_zoom_out)
-            (this.zoom_layer.level <= this.source.zoom_min) ? this.btn_zoom_out.disable() : this.btn_zoom_out.enable();
+        // cancel old timeout
+        if (this._update_timeout)
+            clearTimeout(this._update_timeout);
         
-        this.update_image_link();
+        // trigger in 100ms
+        this._update_timeout = setTimeout(this._update_timeout_trigger.bind(this), 100);  
     },
     
-    // ensure that all tiles that are currently visible are loaded
-    update_tiles: function() {
+    _update_timeout_trigger: function () {
+        // have called update_tiles()
+        this._update_idle = true;
+        
+        this.update_tiles();
+    },
+
+    /** 
+     * Unschedule the call to update_tiles() and call it now, unless it's already been triggered by the previous call to
+     * update_after_timeout
+     */
+    update_now: function () {
+        // abort trigger if active
+        if (this._update_timeout)
+            clearTimeout(this._update_timeout);
+        
+        // update now unless already done
+        if (!this._update_idle)
+            this.update_tiles();
+    },
+
+    /**
+     * Determine the set of visible tiles, and ensure they are loaded
+     */
+    update_tiles: function () {
         // short names for some vars...
         var x = this.scroll_x;
         var y = this.scroll_y;
@@ -384,40 +479,41 @@
         var th = this.source.tile_height;
         var zl = this.zoom_layer.level;
         
-        // figure out what set of columns are visible
+        // figure out which set of cols/rows are visible 
         var start_col = Math.max(0, Math.floor(x / tw));
         var start_row = Math.max(0, Math.floor(y / th));
         var end_col = Math.floor((x + sw) / tw);
         var end_row = Math.floor((y + sh) / th);
 
-        // loop through all those tiles
+        // loop through all visible tiles
         for (var col = start_col; col <= end_col; col++) {
             for (var row = start_row; row <= end_row; row++) {
                 // the tile's id
-                var id = "tile_" + this.zoom_layer.level + "_" + col + "_" + row;
+                var id = "tile_" + zl + "_" + col + "_" + row;
                 
                 // does the element exist already?
+                // XXX: use basic document.getElementById for perf?
                 var t = $(id);
 
                 if (!t) {
                     // build a new tile
                     t = Builder.node("img", {
-                            src:    this.source.build_url(col, row, zl, sw, sh),
+                            src:    this.source.build_url(col, row, zl /* , sw, sh */),
                             id:     id,
-                            title:  "(" + col + ", " + row + ")",
+                            // title:  "(" + col + ", " + row + ")",
                             // style set later
                         }
                     );
                     
-                    // set the CSS style stuff
+                    // position
                     t.style.top = th * row;
                     t.style.left = tw * col;
                     t.style.display = "none";
                     
-                    // wait for it to load
+                    // display once loaded
                     Event.observe(t, "load", _tile_loaded.bindAsEventListener(t));
 
-                    // store the col/row
+                    // remember the col/row
                     t.__col = col;
                     t.__row = row;
                     
@@ -434,17 +530,41 @@
         
         this.update_scroll_ui();
     }, 
-    
-    // update scroll-dependant UI elements
+
+/*
+ * UI state
+ */                  
+
+    /**
+     * Update any zl-dependant UI elements
+     */
+    update_zoom_ui: function () {
+        // deactivate zoom-in button if zoomed in
+        if (this.btn_zoom_in)
+            (this.zoom_layer.level >= this.source.zoom_max) ? this.btn_zoom_in.disable() : this.btn_zoom_in.enable();
+        
+        // deactivate zoom-out button if zoomed out
+        if (this.btn_zoom_out)
+            (this.zoom_layer.level <= this.source.zoom_min) ? this.btn_zoom_out.disable() : this.btn_zoom_out.enable();
+        
+        // link-to-image
+        this.update_image_link();
+    },
+ 
+    /**
+     * Update any position-dependant UI elements
+     */ 
     update_scroll_ui: function () {
+        // update the link-to-this-page thing
+        document.location.hash = "#" + (this.scroll_x + this.center_offset_x) + ":" + (this.scroll_y + this.center_offset_y) + ":" + this.zoom_layer.level;
+        
         // update link-to-image
         this.update_image_link();
-
-        // update the link-to-this-page thing
-        document.location.hash = "#" + (this.scroll_x + this.center_offset_x) + ":" + (this.scroll_y + this.center_offset_y) + ":" + this.zoom_layer.level;
     },
     
-    // update image link with size, zoom, pos
+    /**
+     * Update the link-to-image-of-this-view link with dimensions, zoom, position
+     */
     update_image_link: function () {
         if (!this.image_link)
             return;
@@ -457,41 +577,17 @@
         );
 
         this.image_link.innerHTML = this.view_width + "x" + this.view_height + "@" + this.zoom_layer.level;
-    },
-
-    // do update_tiles after 100ms, unless we are called again
-    update_after_timeout: function () {
-        this._update_idle = false;
-
-        if (this._update_timeout)
-            clearTimeout(this._update_timeout);
-
-        this._update_timeout = setTimeout(this._update_timeout_trigger.bind(this), 100);  
-    },
-    
-    _update_timeout_trigger: function () {
-        this._update_idle = true;
-
-        this.update_tiles();
-    },
-
-    // call update_tiles if it hasn't been called due to update_after_timeout
-    update_now: function () {
-        if (this._update_timeout)
-            clearTimeout(this._update_timeout);
-        
-        if (!this._update_idle)
-            this.update_tiles();
-    },
-
+    }
 });
 
-// used by Viewport.update_tiles to make a tile visible after it has loaded
+/** Used by Viewport.update_tiles to make a tile visible after it has loaded */
 function _tile_loaded (ev) {
     this.style.display = "block";
 }
 
-// a zoom layer containing the tiles for one zoom level
+/**
+ * A zoom layer contains a (col, row) grid of tiles at a specific zoom level. 
+ */
 var ZoomLayer = Class.create({
     initialize: function (source, zoom_level) {
         this.source = source;
@@ -502,28 +598,33 @@
         this.tiles = [];
     },
     
-    // add a tile to this zoom layer
+    /** Add a tile to this zoom layer's grid */
     add_tile: function (tile) {
         this.div.appendChild(tile);
         this.tiles.push(tile);
     },
     
-    // make this zoom layer visible with the given z-index
+    /** Make this zoom layer visible with the given z-index */
     enable: function (z_index) {
         this.div.style.zIndex = z_index;
         this.div.show();
     },
    
-    // hide this zoom layer
-    disable: function (z_index) {
+    /** Hide this zoom layer */
+    disable: function () {
         this.div.hide();
     },
     
-    // update the tiles in this zoom layer so that they are in the correct position and of the correct size when
-    // viewed with the given zoom level
+    /** 
+     * Update the tile grid in this zoom layer such the tiles are in the correct position and of the correct size
+     * when viewed with the given zoom level.
+     *
+     * For zoom levels different than this layer's level, this will resize the tiles!
+     */
     update_tiles: function (zoom_level) {
         var zd = zoom_level - this.level;
-
+        
+        // desired tile size
         var tw = scaleByZoomDelta(this.source.tile_width, zd);
         var th = scaleByZoomDelta(this.source.tile_height, zd);
 
@@ -531,19 +632,18 @@
         var tiles_len = tiles.length;
         var t, ts;
         
+        // XXX: *all* tiles? :/
         for (var i = 0; i < tiles_len; i++) {
             t = tiles[i];
             ts = t.style;
-
+            
+            // reposition
             ts.width = tw;
             ts.height = th;
-            ts.top = th*t.__row;
-            ts.left = tw*t.__col;
+            ts.top = th * t.__row;
+            ts.left = tw * t.__col;
         }
-    },
-
-
-
+    }
 });
 
 // scale the given co-ordinate by a zoom delta. If we zoom in (dz > 0), n will become larger, and if we zoom