static/tiles.js
changeset 13 a0cb32f3de3d
equal deleted inserted replaced
12:aa6b83c94528 13:a0cb32f3de3d
       
     1 // A source of tile images of a specific width/height, zoom level range, and some other attributes
       
     2 var Source = Class.create({
       
     3     initialize: function (path, tile_width, tile_height, zoom_min, zoom_max) {
       
     4         // relative path?
       
     5         if (path.substring(0, 1) != "/") {
       
     6             // current document path
       
     7             _path = document.location.pathname;
       
     8 
       
     9             // as relative
       
    10             _path = _path.substring(0, _path.lastIndexOf("/"));
       
    11             
       
    12             // conact to make absolute path
       
    13             path = _path + "/" + path
       
    14 
       
    15         }
       
    16         
       
    17         // store
       
    18         this.path = path;
       
    19         this.tile_width = tile_width;
       
    20         this.tile_height = tile_height;
       
    21         this.zoom_min = zoom_min;
       
    22         this.zoom_max = zoom_max;
       
    23 
       
    24         this.refresh = false;
       
    25         this.opt_key = this.opt_value = null;
       
    26     },
       
    27     
       
    28     // build a URL for the given tile image
       
    29     build_url: function (col, row, zl, sw, sh) {
       
    30         // two-bit hash (0-4) based on the (col, row)
       
    31         var hash = ( (col % 2) << 1 | (row % 2) ) + 1;
       
    32         
       
    33         // the subdomain to use
       
    34         var subdomain = "";
       
    35         
       
    36         if (0)
       
    37             subdomain = "tile" + hash + ".";
       
    38 
       
    39         // the (x, y) co-ordinates
       
    40         var x = col * this.tile_width;
       
    41         var y = row * this.tile_height;
       
    42 
       
    43         var url = "http://" + subdomain + document.location.host + this.path + "?x=" + x + "&y=" + y + "&z=" + zl + "&sw=" + sw + "&sh=" + sh;
       
    44 
       
    45         if (this.refresh)
       
    46             url += "&ts=" + new Date().getTime();
       
    47 
       
    48         if (this.opt_key && this.opt_value)
       
    49             url += "&" + this.opt_key + "=" + this.opt_value;
       
    50 
       
    51         return url;
       
    52     },
       
    53 });
       
    54 
       
    55 // a viewport that contains a substrate which contains several zoom layers which contain many tiles
       
    56 var Viewport = Class.create({
       
    57     initialize: function (source, viewport_id) {
       
    58         this.source = source;
       
    59 
       
    60         this.id = viewport_id;
       
    61         this.div = $(viewport_id);
       
    62         this.substrate = this.div.down("div.substrate");
       
    63     
       
    64         // the stack of zoom levels
       
    65         this.zoom_layers = [];
       
    66         
       
    67         // pre-populate the stack
       
    68         for (var zoom_level = source.zoom_min; zoom_level <= source.zoom_max; zoom_level++) {
       
    69             var zoom_layer = new ZoomLayer(source, zoom_level);
       
    70 
       
    71             this.substrate.appendChild(zoom_layer.div);
       
    72             this.zoom_layers[zoom_level] = zoom_layer;
       
    73         }
       
    74 
       
    75         // make the substrate draggable
       
    76         this.draggable = new Draggable(this.substrate, {
       
    77             onStart: this.on_scroll_start.bind(this),
       
    78             onDrag: this.on_scroll_move.bind(this),
       
    79             onEnd: this.on_scroll_end.bind(this),
       
    80         });
       
    81 
       
    82         // event handlers
       
    83         Event.observe(this.substrate, "dblclick", this.on_dblclick.bindAsEventListener(this));
       
    84         Event.observe(this.substrate, "mousewheel", this.on_mousewheel.bindAsEventListener(this));
       
    85         Event.observe(this.substrate, "DOMMouseScroll", this.on_mousewheel.bindAsEventListener(this));     // mozilla
       
    86         Event.observe(document, "resize", this.on_resize.bindAsEventListener(this));
       
    87 
       
    88         // load initial view
       
    89         this.update_size();
       
    90 
       
    91         // this sets the scroll offsets, zoom level, and loads the tiles
       
    92         this.zoom_to(0, 0, 0);
       
    93     },
       
    94     
       
    95     /* event handlers */
       
    96 
       
    97     // window resized
       
    98     on_resize: function (ev) {
       
    99         this.update_size();
       
   100         this.update_tiles();
       
   101     },
       
   102     
       
   103     // double-click handler
       
   104     on_dblclick: function (ev) {
       
   105         var offset = this.event_offset(ev);
       
   106         
       
   107         this.zoom_center_to(
       
   108             this.scroll_x + offset.x,
       
   109             this.scroll_y + offset.y,
       
   110             1   // zoom in
       
   111         );
       
   112     },
       
   113 
       
   114     // mousewheel handler
       
   115     on_mousewheel: function (ev) {
       
   116         // this works in very weird ways, so it's based on code from http://adomas.org/javascript-mouse-wheel/
       
   117         // (it didn't include any license, so this is written out manually)
       
   118         var delta;
       
   119         
       
   120         // this is browser-dependant...
       
   121         if (ev.wheelDelta) {
       
   122             // IE + Opera
       
   123             delta = ev.wheelDelta;
       
   124 
       
   125             if (window.opera) {  
       
   126                 // Opera, but apparently not newer versions?
       
   127                 //delta = -delta;
       
   128             }
       
   129 
       
   130         } else if (ev.detail) {
       
   131             // Mozilla
       
   132             delta = -ev.detail;
       
   133 
       
   134         } else {
       
   135             // mousewheel not supported...
       
   136             return;
       
   137 
       
   138         }
       
   139         
       
   140         // don't scroll the page
       
   141         if (ev.preventDefault)
       
   142             ev.preventDefault();
       
   143         
       
   144         // delta > 0 : scroll up, zoom in
       
   145         // delta < 0 : scroll down, zoom out
       
   146         delta = delta < 0 ? -1 : 1;
       
   147 
       
   148         // Firefox's DOMMouseEvent's pageX/Y attributes are broken. layerN is for mozilla, offsetN for IE, seems to work
       
   149 
       
   150         // absolute location of the cursor
       
   151         var x = parseInt(ev.target.style.left) + (ev.layerX ? ev.layerX : ev.offsetX);
       
   152         var y = parseInt(ev.target.style.top) + (ev.layerY ? ev.layerY : ev.offsetY);
       
   153         
       
   154         // zoom \o/
       
   155         this.zoom_center_to(x, y, delta);
       
   156     },
       
   157     
       
   158     // substrate scroll was started
       
   159     on_scroll_start: function (ev) {
       
   160 
       
   161     },
       
   162     
       
   163     // substrate was scrolled, update scroll_{x,y}, and then update tiles after 100ms
       
   164     on_scroll_move: function (ev) {
       
   165         this.update_scroll();
       
   166         this.update_after_timeout();
       
   167     },
       
   168     
       
   169     // substrate scroll was ended, update tiles now
       
   170     on_scroll_end: function (ev) {
       
   171         this.update_now();
       
   172     },
       
   173 
       
   174     /* get state */
       
   175 
       
   176     // return the absolute (x, y) coords of the given event inside the viewport
       
   177     event_offset: function (ev) {
       
   178         var offset = this.div.cumulativeOffset();
       
   179 
       
   180         return {
       
   181             x: ev.pointerX() - offset.left, 
       
   182             y: ev.pointerY() - offset.top
       
   183         };
       
   184     },
       
   185 
       
   186     /* modify state */
       
   187 
       
   188     // scroll the view to place the given absolute (x, y) co-ordinate at the top left
       
   189     scroll_to: function (x, y) {
       
   190         // update it via the style
       
   191         this.substrate.style.top = "-" + y + "px";
       
   192         this.substrate.style.left = "-" + x + "px";
       
   193         
       
   194         // update these as well
       
   195         this.scroll_x = x;
       
   196         this.scroll_y = y;
       
   197     },
       
   198 
       
   199     // scroll the view to place the given absolute (x, y) co-ordinate at the center
       
   200     scroll_center_to: function (x, y) {
       
   201         return this.scroll_to(
       
   202             x - this.center_offset_x,
       
   203             y - this.center_offset_y
       
   204         );
       
   205     },
       
   206  
       
   207     // zoom à la delta such that the given (zoomed) absolute (x, y) co-ordinates will be at the top left
       
   208     zoom_scaled: function (x, y, delta) {
       
   209         if (!this.update_zoom(delta))
       
   210             return false;
       
   211 
       
   212         // scroll to the new position
       
   213         this.scroll_to(x, y);
       
   214         
       
   215         // update view
       
   216         // XXX: ...
       
   217         this.update_after_timeout();
       
   218         
       
   219         return true;
       
   220     },
       
   221    
       
   222     // zoom à la delta such that the given (current) absolute (x, y) co-ordinates will be at the top left
       
   223     zoom_to: function (x, y, delta) {
       
   224         return this.zoom_scaled(
       
   225             scaleByZoomDelta(x, delta),
       
   226             scaleByZoomDelta(y, delta),
       
   227             delta
       
   228         );
       
   229     },
       
   230     
       
   231     // zoom à la delta such that the given (current) absolute (x, y) co-ordinates will be at the center
       
   232     zoom_center_to: function (x, y, delta) {
       
   233         return this.zoom_scaled(
       
   234             scaleByZoomDelta(x, delta) - this.center_offset_x,
       
   235             scaleByZoomDelta(y, delta) - this.center_offset_y,
       
   236             delta
       
   237         );
       
   238     },
       
   239 
       
   240 
       
   241     /* update view/state to reflect reality */
       
   242 
       
   243     // figure out the viewport dimensions
       
   244     update_size: function () {
       
   245         this.view_width = this.div.getWidth();
       
   246         this.view_height = this.div.getHeight();
       
   247 
       
   248         this.center_offset_x = Math.floor(this.view_width / 2);
       
   249         this.center_offset_y = Math.floor(this.view_height / 2);
       
   250     },
       
   251     
       
   252     // figure out the scroll offset as absolute pixel co-ordinates at the top left
       
   253     update_scroll: function() {
       
   254         this.scroll_x = -parseInt(this.substrate.style.left);
       
   255         this.scroll_y = -parseInt(this.substrate.style.top);
       
   256     },
       
   257 
       
   258     // wiggle the ZoomLevels around to match the current zoom level
       
   259     update_zoom: function(delta) {
       
   260         if (!this.zoom_layer) {
       
   261             // first zoom operation
       
   262 
       
   263             // is the new zoom level valid?
       
   264             if (!this.zoom_layers[delta])
       
   265                 return false;
       
   266             
       
   267             // set the zoom layyer
       
   268             this.zoom_layer = this.zoom_layers[delta];
       
   269             
       
   270             // enable it
       
   271             this.zoom_layer.enable(11);
       
   272             
       
   273             // no need to .update_tiles or anything like that
       
   274             
       
   275         } else {
       
   276             // is the new zoom level valid?
       
   277             if (!this.zoom_layers[this.zoom_layer.level + delta])
       
   278                 return false;
       
   279 
       
   280             var zoom_old = this.zoom_layer;
       
   281             var zoom_new = this.zoom_layers[this.zoom_layer.level + delta];
       
   282             
       
   283             // XXX: ugly hack
       
   284             if (this.zoom_timer) {
       
   285                 clearTimeout(this.zoom_timer);
       
   286                 this.zoom_timer = null;
       
   287             }
       
   288             
       
   289             // get other zoom layers out of the way
       
   290             this.zoom_layers.each(function (zl) {
       
   291                 zl.disable();
       
   292             });
       
   293             
       
   294             // update the zoom layer
       
   295             this.zoom_layer = zoom_new;
       
   296             
       
   297             // apply new z-indexes, preferr the current one over the new one
       
   298             zoom_new.enable(11);
       
   299             zoom_old.enable(10);
       
   300             
       
   301             // resize the tiles in the two zoom layers
       
   302             zoom_new.update_tiles(zoom_new.level);
       
   303             zoom_old.update_tiles(zoom_old.level);
       
   304             
       
   305             // XXX: ugly hack
       
   306             this.zoom_timer = setTimeout(function () { zoom_old.disable()}, 1000);
       
   307         }
       
   308 
       
   309         return true;
       
   310     },
       
   311     
       
   312     // ensure that all tiles that are currently visible are loaded
       
   313     update_tiles: function() {
       
   314         // short names for some vars...
       
   315         var x = this.scroll_x;
       
   316         var y = this.scroll_y;
       
   317         var sw = this.view_width;
       
   318         var sh = this.view_height;
       
   319         var tw = this.source.tile_width;
       
   320         var th = this.source.tile_height;
       
   321         var zl = this.zoom_layer.level;
       
   322         
       
   323         // figure out what set of columns are visible
       
   324         var start_col = Math.max(0, Math.floor(x / tw));
       
   325         var start_row = Math.max(0, Math.floor(y / th));
       
   326         var end_col = Math.floor((x + sw) / tw);
       
   327         var end_row = Math.floor((y + sh) / th);
       
   328 
       
   329         // loop through all those tiles
       
   330         for (var col = start_col; col <= end_col; col++) {
       
   331             for (var row = start_row; row <= end_row; row++) {
       
   332                 // the tile's id
       
   333                 var id = "tile_" + this.zoom_layer.level + "_" + col + "_" + row;
       
   334                 
       
   335                 // does the element exist already?
       
   336                 var t = $(id);
       
   337 
       
   338                 if (!t) {
       
   339                     // build a new tile
       
   340                     t = Builder.node("img", {
       
   341                             src:    this.source.build_url(col, row, zl, sw, sh),
       
   342                             id:     id,
       
   343                             title:  "(" + col + ", " + row + ")",
       
   344                             // style set later
       
   345                         }
       
   346                     );
       
   347                     
       
   348                     // set the CSS style stuff
       
   349                     t.style.top = th * row;
       
   350                     t.style.left = tw * col;
       
   351                     t.style.display = "none";
       
   352                     
       
   353                     // wait for it to load
       
   354                     Event.observe(t, "load", _tile_loaded.bindAsEventListener(t));
       
   355 
       
   356                     // store the col/row
       
   357                     t.__col = col;
       
   358                     t.__row = row;
       
   359                     
       
   360                     // add it to the zoom layer
       
   361                     this.zoom_layer.add_tile(t);
       
   362 
       
   363                 } else if (this.source.reload) {
       
   364                     // force the tile to reload
       
   365                     touch_tile(t, col, row);
       
   366 
       
   367                 }
       
   368             }
       
   369         }
       
   370 
       
   371 /* XXX: fixme
       
   372         // update the link-to-this-page thing
       
   373         document.location.hash = "#goto_" + (x + g_w_half) + ":" + g_w + "_" + (y + g_h_half) + ":" + g_h + "_" + g_z;
       
   374 */
       
   375 
       
   376     }, 
       
   377 
       
   378     // do update_tiles after 100ms, unless we are called again
       
   379     update_after_timeout: function () {
       
   380         this._update_idle = false;
       
   381 
       
   382         if (this._update_timeout)
       
   383             clearTimeout(this._update_timeout);
       
   384 
       
   385         this._update_timeout = setTimeout(this._update_timeout_trigger.bind(this), 100);  
       
   386     },
       
   387     
       
   388     _update_timeout_trigger: function () {
       
   389         this._update_idle = true;
       
   390 
       
   391         this.update_tiles();
       
   392     },
       
   393 
       
   394     // call update_tiles if it hasn't been called due to update_after_timeout
       
   395     update_now: function () {
       
   396         if (this._update_timeout)
       
   397             clearTimeout(this._update_timeout);
       
   398         
       
   399         if (!this._update_idle)
       
   400             this.update_tiles();
       
   401     },
       
   402 
       
   403 });
       
   404 
       
   405 // used by Viewport.update_tiles to make a tile visible after it has loaded
       
   406 function _tile_loaded (ev) {
       
   407     this.style.display = "block";
       
   408 }
       
   409 
       
   410 // a zoom layer containing the tiles for one zoom level
       
   411 var ZoomLayer = Class.create({
       
   412     initialize: function (source, zoom_level) {
       
   413         this.source = source;
       
   414         this.level = zoom_level;
       
   415         this.div = Builder.node("div", { id: "zl_" + this.level, style: "position: relative; display: none;"});
       
   416 
       
   417         // our tiles
       
   418         this.tiles = [];
       
   419     },
       
   420     
       
   421     // add a tile to this zoom layer
       
   422     add_tile: function (tile) {
       
   423         this.div.appendChild(tile);
       
   424         this.tiles.push(tile);
       
   425     },
       
   426     
       
   427     // make this zoom layer visible with the given z-index
       
   428     enable: function (z_index) {
       
   429         this.div.style.zIndex = z_index;
       
   430         this.div.show();
       
   431     },
       
   432    
       
   433     // hide this zoom layer
       
   434     disable: function (z_index) {
       
   435         this.div.hide();
       
   436     },
       
   437     
       
   438     // update the tiles in this zoom layer so that they are in the correct position and of the correct size when
       
   439     // viewed with the given zoom level
       
   440     update_tiles: function (zoom_level) {
       
   441         var zd = zoom_level - this.level;
       
   442 
       
   443         var tw = scaleByZoomDelta(this.source.tile_width, zd);
       
   444         var th = scaleByZoomDelta(this.source.tile_height, zd);
       
   445 
       
   446         var tiles = this.tiles;
       
   447         var tiles_len = tiles.length;
       
   448         var t, ts;
       
   449         
       
   450         for (var i = 0; i < tiles_len; i++) {
       
   451             t = tiles[i];
       
   452             ts = t.style;
       
   453 
       
   454             ts.width = tw;
       
   455             ts.height = th;
       
   456             ts.top = th*t.__row;
       
   457             ts.left = tw*t.__col;
       
   458         }
       
   459     },
       
   460 
       
   461 
       
   462 
       
   463 });
       
   464 
       
   465 // scale the given co-ordinate by a zoom delta. If we zoom in (dz > 0), n will become larger, and if we zoom 
       
   466 // out (dz < 0), n will become smaller.
       
   467 function scaleByZoomDelta (n, dz) {
       
   468     if (dz > 0)
       
   469         return n << dz;
       
   470     else
       
   471         return n >> -dz;
       
   472 }
       
   473