static/tiles.js
author Tero Marttila <terom@fixme.fi>
Mon, 07 Jul 2008 04:36:03 +0300
changeset 27 1e79b4cc8f1b
permissions -rw-r--r--
support for static files (.css, .html, .js), and tiles - serves up a full viewer at / now, but the JS code needs cleaning up

committer: Tero Marttila <terom@fixme.fi>
// our initial (col, row) position
var g_x, g_y;

// how many pixels (wide, high) the viewport is
var g_w, g_h;

// how (wide, high) a tile is
var g_tw, g_th;

// half of viewport width/height
var g_w_half, g_h_half;

// the Draggable substrate, we get our current offset from this
var g_draggable;

// random debugging crap
var g_debug, g_debug_enabled;

// our current zoom level
var g_z;

// minimum and maximum zoom levels
var g_z_min, g_z_max;

// the viewport and substrate divs
var viewp, subs;

// the timeout used to call check_tiles
var g_timeout;

// a flag that signifies if we have updated the map due to being idle (not moving for 100ms)
var g_idle;

// a list of tiles for each zoom level
var g_tiles;

// any target that we were given in the URL
var g_target;

// called with info about the viewport
function init () {
    g_debug_enabled = false;
    fullscreen = false;

    viewp = $("viewport");
    subs = $("substrate");

    // were we anchored to some particular location?
    g_target = document.location.hash.replace("#", "");
   
    // create the draggable
    g_draggable = new Draggable("substrate", {
        starteffect: null, 
        endeffect: null,
        onStart: viewport_scroll_start,
        onDrag: viewport_scroll_move,
        onEnd: viewport_scroll_done
    });

    // double-click listener
    Event.observe(subs, "dblclick", viewport_dblclick);

    // mouse wheel
    Event.observe(subs, "mousewheel", viewport_mousewheel);
    Event.observe(subs, "DOMMouseScroll", viewport_mousewheel);     // mozilla

    // window size changes
    Event.observe(document, "resize", update_viewport_size);

    // should we do debugging?
    if (g_debug_enabled) {
        g_debug = document.createElement("pre");
        g_debug.style.height = "100px";
        g_debug.style.overflow = "auto";
        debug("Debug output...");

        $('wrapper').appendChild(g_debug);
    }
}

/*
 * Initialize this for viewing the given image parameters
 */

var g_opt_key, g_opt_value, g_refresh;
function load (x, y, tw, th, z, z_min, z_max, opt_key, opt_value, refresh) {
    // variable setup
    g_x = x;
    g_y = y;
    g_tw = tw;
    g_th = th;
    g_z = z;
    g_z_min = z_min;
    g_z_max = z_max;
    g_opt_key = opt_key;
    g_opt_value = opt_value;
    g_refresh = refresh;
    
    g_idle = true;

    g_tiles = [];
    
    viewp = $("viewport");
    subs = $("substrate");
}

function _onload () {
    // create the zoom-level divs
    for (var zl = g_z_min; zl <= g_z_max; zl++) {
        zl_div = document.createElement("div");
        zl_div.id = "zl_" + zl;
        zl_div.style.position = "relative";

        subs.appendChild(zl_div);

        g_tiles[zl] = [];
    }

    if (g_target.indexOf("goto") == 0) {
        var asdf = g_target.split("_", 4);
        var x_w = asdf[1].split(":", 2);
        var y_h = asdf[2].split(":", 2);
        
        update_zoom_level(
            parseInt(asdf[3]) - g_z
        );
        
        update_viewport_size(true);
        
        g_w = parseInt(x_w[1]);
        g_h = parseInt(y_h[1]);

        scroll_center_to(
            parseInt(x_w[0]),
            parseInt(y_h[0])
        );

        check_tiles();
        
        // no fullscreen for goto mode
        return;

    } else {
        if (g_target == "fullscreen")
            fullscreen = true;

        // scroll to the initial position
        scroll_to(g_x*g_tw, g_y*g_th);
        
        // adjust the zoom buttons
        update_zoom_level(0);
    }
    
    if (fullscreen)
        viewport_fullscreen();
    else  {
        // load the viewport size and then the tiles
        update_viewport_size(false);
    }
}

Event.observe(window, "load", _onload);

function unload () {
    $("substrate").innerHTML = "";
    g_tiles = [];
}


function debug (str) {
    if (g_debug_enabled)
        g_debug.textContent = (str + "\n") + g_debug.textContent;
}

// viewport-oriented stuff

/*
 * update the screen size stuff based on the actual viewport size
 */
function update_viewport_size (no_load) {
    g_w = viewp.getWidth();
    g_h = viewp.getHeight();

    g_w_half = Math.floor(g_w/2);
    g_h_half = Math.floor(g_h/2);
    
    if (!no_load)
        check_tiles();
}

function viewport_fullscreen () {
    viewp.style.position = "absolute";
    viewp.style.top = "0px";
    viewp.style.left = "0px";
//    viewp.style.bottom = "0px";
//    viewp.style.right = "0px";
    viewp.style.width = "100%";
    viewp.style.height = "100%";
    viewp.style.borderWidth = 0;

    update_viewport_size(false);
}

// pixel-oriented stuff, related to where the view is scrolled to

/*
 * scroll the view to a given (x, y) pixel offset from the top-left corner
 */
function scroll_to (x, y) {
    subs.style.top = "-" + y + "px";
    subs.style.left = "-" + x + "px";
}

function scroll_center_to (x, y) {
    return scroll_to(
        x - g_w_half,
        y - g_h_half
    );
}

/*
 * Move the view dx pixels to the right, and dy pixels to the bottom in a fancy animated fashion
 */
function move (dx, dy) {
    new Effect.Move(subs, {
        x: -dx,
        y: -dy,
        duration: 0.5,  // pretty quick
        afterFinish: check_tiles
    });
}

/*
 * return the current horizontal scroll offset in pixels from the left edge
 */
function scroll_x () {
    return -parseInt(subs.style.left);
}

/*
 * return the current vertical scroll offset in pixels from the top edge
 */
function scroll_y () {
    return -parseInt(subs.style.top);
}

/*
 * scale co-ordinates by a zoom factor, if we zoom in (dz > 0), n will become larger, and if we zoom out (dz < 0), n will become smaller
 */
function scaleByZoomDelta (n, dz) {
    if (dz > 0)
        return n << dz;
    else
        return n >> -dz;
}

/*
 * From the given offset, calcuate the new_offset that would be needed for the
 * pixel at (offset+half) to be at (new_offset+half) after the given zoom delta
 * has been applied
 */
function align_center(offset, half, delta) {
    return scaleByZoomDelta(offset + half, delta) - half;
}

/*
 * change the zoom level. A positive value zooms out, a negative vlaue zooms in.
 */
function zoom (delta) {
    return zoom_center_to(
        scroll_x() + g_w_half,
        scroll_y() + g_h_half, 
        delta
    );
}

/*
 * Zoom in/out such that the given co-ord (in current co-ord values) will be in the center of the screen
 */
function zoom_center_to (x, y, delta) {
    return zoom_to(
        scaleByZoomDelta(x, delta) - g_w_half,
        scaleByZoomDelta(y, delta) - g_h_half,
        delta
     );
}

/*
 * Zoom in/out such that the given co-ord (in target co-ord values) will be in the top-left corner
 */
function zoom_to (x, y, delta) {
    if (!update_zoom_level(delta))
        return false;

    // scroll to a new position such that the center co-ordinate is correct
    scroll_to(x, y);
    
    // update view
    update_after_timeout();

    return true;
}

/*
 * Return the (x, y) co-ord of the event inside the viewport
 */
function event_offset (e) {
    var offset = viewp.cumulativeOffset();

    return {
        x: e.pointerX() - offset.left, 
        y: e.pointerY() - offset.top
    };
}

/*
 * Double-click handler
 */
function viewport_dblclick (e) {
    var offset = event_offset(e);
    
    if (!zoom_center_to(
        scroll_x() + offset.x,
        scroll_y() + offset.y,
        1
    )) {
        // if we're already zoomed in, move o/
        move(offset.x - g_w_half, offset.y - g_h_half);
    }

}

// zoom control stuff

/*
 * Mouse wheel handler
 */
function viewport_mousewheel (e) {
    // this works in very weird ways, so it's based on code from http://adomas.org/javascript-mouse-wheel/ (stupid person didn't include any license)
    var delta;

    if (e.wheelDelta) {  // IE + Opera
        delta = e.wheelDelta;
//        if (window.opera)   // Opera, but apparently not newer versions?
//            delta = -delta;
    } else if (e.detail) {  // Mozilla
        delta = -e.detail;
    } else {
        return;
    }

    if (e.preventDefault)
        e.preventDefault();
    
    // delta > 0 : scroll up, zoom in
    // delta < 0 : scroll down, zoom out
    delta = delta < 0 ? -1 : 1;

    // Firefox's DOMMouseEvent's pageX/Y attributes are broken. layerN is for mozilla, offsetN for IE, seems to work
    var x = parseInt(e.target.style.left) + (e.layerX ? e.layerX : e.offsetX);
    var y = parseInt(e.target.style.top) + (e.layerY ? e.layerY : e.offsetY);
    var dx = x - scroll_x();
    var dy = y - scroll_y();

    zoom_to(
        scaleByZoomDelta(x, delta) - dx, 
        scaleByZoomDelta(y, delta) - dy, 
        delta
    );

//  if ( )    
//        debug("scrollzoom from x=" + x + " y=" + y);
}

var g_zoom_timer;

/*
 * Updates the zoom level with the given delta. Returns true/false if it's valid or not
 */
function update_zoom_level (delta) {
    var oz = g_z;
    var z = g_z + delta;
    
    // is the new zoom level valid?
    if (z < g_z_min || z > g_z_max)
        return false;
    
    // hmm...
    if (g_zoom_timer) {
        clearTimeout(g_zoom_timer);
        g_zoom_timer = null;
    }

    g_z = z;
    
    // now update the zoomlevel div's z-indexes
    zoom_showhide_fillers(false);
    var zi = 10;
    var i;
    
    // preferr the current one over the new one
    $("zl_" + z).style.zIndex = 11;
    $("zl_" + oz).style.zIndex = 10;
    
    $("zl_" + z).show();
    $("zl_" + oz).show();

    // resize them
    for (zi = Math.min(z, oz); zi <= Math.max(z, oz); zi++) {
        dz = z - zi;

        w = scaleByZoomDelta(g_tw, dz);
        h = scaleByZoomDelta(g_th, dz);

        tiles = g_tiles[zi];
        tiles_len = tiles.length;

        for (i = 0; i < tiles_len; i++) {
            t = tiles[i];
            ts = t.style;

            ts.width = w;
            ts.height = h;
            ts.top = h*t.__row;
            ts.left = w*t.__col;
        }
    }
    
    if (z != oz)
        g_zoom_timer = setTimeout(function () { $("zl_" + oz).hide()}, 1000);
    
    return true;
}

/*
 * Hide the filler layers, i.e. the ones that aren't the current zoom level
 */
function zoom_showhide_fillers (show) {
    for (var zi = g_z_min; zi <= g_z_max; zi++)
        if (zi != g_z)
            if (show)
                $("zl_" + zi).show();
            else 
                $("zl_" + zi).hide();
}

// tile-oriented stuff

/*
 * Return the URL to the given tile, taking the current zoom level into account
 */
function build_url (col, row) {
    var x = col*g_tw;
    var y = row*g_th;
    
    // two-bit hash (0-4)
    var h = ((col%2)<<1 | (row%2)) + 1;

    var u = "http://" + /* "tile" + h + "." + */ document.location.host + "/tile?x=" + x + "&y=" + y + "&z=" + g_z + "&sw=" + g_w + "&sh=" + g_h;

    if (g_refresh)
        u += "&ts=" + new Date().getTime();

    if (g_opt_key && g_opt_value)
        u += "&" + g_opt_key + "=" + g_opt_value;

    return u;
}

/*
 * Loads the given tile, assuming that it hasn't been loaded yet
 */
function load_tile (id, col, row) {
    if (col < 0 || row < 0)
        return;

    e = document.createElement("img");
    e.src = build_url(col, row);
    e.id = id;
    e.title = "(" + col + ", " + row + ")"
    e.style.top = g_th * row;
    e.style.left = g_tw * col;
    e.style.display = "none";
    Event.observe(e, "load", _tile_loaded);
    e.__col = col;
    e.__row = row;

    $("zl_" + g_z).appendChild(e);
    g_tiles[g_z].push(e);
}

function _tile_loaded (e) {
    this.style.display = "block";

}

/*
 * Updates the tile to the current time/zoom level
 */
function touch_tile (tile, col, row) {
    if (g_refresh)
        tile.src = build_url(col, row);
}

/*
 * Compute which tiles are currently visible, and load/touch all of them.
 *
 * If we are standing still, will schedule another call in two seconds
 */
function check_tiles () {
    var x = scroll_x();
    var y = scroll_y();
    var w = g_w;
    var h = g_h;
    
    var start_col = Math.floor(x/g_tw);
    var start_row = Math.floor(y/g_th);
    var end_col = Math.floor((x + w)/g_tw);
    var end_row = Math.floor((y + h)/g_th);

//    debug("Visible area: (" + x + ", " + y + ") -> (" + (x+w) + ", " + (y+h) + "), visible tiles: (" + start_col + ", " + start_row + ") -> (" + end_col + ", " + end_row + ")");

    var id, t;

    for (col = start_col; col <= end_col; col++) {
        for (row = start_row; row <= end_row; row++) {
            id = "tile_" + g_z + "_" + col + "_" + row;
            t = $(id);

            if (t)
                touch_tile(t, col, row);
            else
                load_tile(id, col, row);
        }
    }

/* XXX: fix this...
    // update the link-to-this-page thing
    document.location.hash = "#goto_" + (x + g_w_half) + ":" + g_w + "_" + (y + g_h_half) + ":" + g_h + "_" + g_z;
*/    
}

// delayed updates

/*
 * Call check_tiles in 100ms, unless we are called again
 */
function update_after_timeout () {
    g_idle = false;

    if (g_timeout)
        clearTimeout(g_timeout);

    g_timeout = setTimeout(_update_timeout, 100);  
}

function update_timeout_cancel () {
    if (g_timeout) {
        debug("Cancel timeout");
        clearTimeout(g_timeout);
    }
}

function _update_timeout () {
    g_idle = true;

    check_tiles();
}
/*
 * call check_tiles if it hasn't been called due to update_after_timeout
 */
function update_now () {
    if (g_timeout)
        clearTimeout(g_timeout);
    
    if (!g_idle)
        check_tiles();
}

// scrolling

/*
 * called on scroll start
 */
var g_scroll_x, g_scroll_y;
function viewport_scroll_start () {
    g_scroll_x = scroll_x();
    g_scroll_y = scroll_y();
}

function viewport_scroll_move () {
    update_after_timeout();

    // may still want some kind of code like this later
    // possibly: update once we scroll over 100px, regardless of if we're still or not
/*    
    var x = scroll_x();
    var y = scroll_y();

    var dx = Math.abs(g_scroll_x - x);
    var dy = Math.abs(g_scroll_y - y)

    if (dx > 100 || dy > 100) {
        debug("scrolled dx=" + dx + " dy=" + dy + " pixels, update in 100ms");

        update_after_timeout();
    }
*/
}

function viewport_scroll_done() {
    update_now();
}