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