# HG changeset patch # User Tero Marttila # Date 1263036400 -7200 # Node ID 0ce4064c428e07351d3c329a3713717f21bc1209 # Parent 1a93b5a6efd0e39b1b8dac82de1b654ca24fcce4 cleanup tiles2.js, fix scaling of old layer when zooming, and have double-click always center diff -r 1a93b5a6efd0 -r 0ce4064c428e static/tiles2.js --- 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