Yesterday I visited Linden Lab, the creators of Second Life, with a few friends. Very neat! They're just about moved into their new office, looks like; their previous one was a lot closer to ours, but we can certainly relate to outgrowing a space.
At Cienna Rand's suggestion, I've been building a site to classify and share Second Life landmarks, Landmarker. On the writing side, you can post landmarks, write a description, add lightweight categories (tags), and link to pictures taken of that place with Snapzilla or Flickr (*cough* TypePad *cough*). For reading, you can browse by author, tag, or in-world geography. You can also add authors and tags to your "inbox" to check more easily.
The functionality (tagging, inbox) is mostly mimicked from del.icio.us (as it says in the footer of every page...). I'm also taking a cue from the original AllConsuming by integrating as much functionality from other sites as possible. For example, you can't upload pictures to Landmarker; you have to send them to Snapzilla or Flickr or your own web site, then link to them. I plan to add Atom posting of new landmarks, to automatically post them to a sidebar (*cough* TypeList *cough*). Relatedly, I've been using a Ta-da List to track bugs and features I've not yet added.
It's interesting to see where the differences in the content objects (virtual places and web sites) create differences in the sites. One of the differences in del.icio.us and Landmarker is SL places have geography. If web pages are spacially arranged, it's in a directional graph, with all the fluidity that effects. In Second Life, if you're at point A and want to be at point C, you have to go through point B first. (Unless you're going to point Z, in which case you can teleport to point V--but you still have to deal with W, X, and Y.)
Oddly enough, there's already a different Ajaxy site that deals in geography. Coincidentally with my visit, I implemented a similar system on Landmarker this week. You can see it in action in IE 6 or Firefox (Cienna told me it doesn't work right in Safari yet, and I need to fix it up in IE before attempting Opera).
The Lindens I spoke to were excited about it--embarrassingly so, given how little work I had had to put in, and how much cooler stuff everyone else I was with had done actually in Second Life. So for your reference, dear reader, I thought I might describe how it works.
Earlier this week I added CSS and JavaScript to allow you to scroll the map around. Here's a diagram of what's actually going on:

The map you see is actually a "viewport" container with certain CSS:
#map-viewport {
position: relative;
overflow: hidden;
width: 512px; height: 384px;
}
Setting position: relative allows me to absolutely position the elements inside it, which is exactly what I do with the "substrate" inside the viewport:
#map-substrate {
position: absolute;
top: 0; left: 0;
}
All the actual map squares (in Landmarker, these are the sim maps I made, but in Google Maps they would be arbitrary chunks of land) are attached onto the substrate with absolute positioning for each image:
#map-viewport .sim { position: absolute; }
<img class="sim" id="sim-23"
src="/maps/255488,257536.png"
width="128" height="128"
style="bottom: 38; left: 202"
alt="Aqua" title="Aqua" />
Then JavaScript event handlers are placed on the viewport to move the substrate around when you move the mouse:
var map_vport = document.getElementById('map-viewport');
var map_substrate = document.getElementById('map-substrate');
// explicitly set the substrate location so we can read it later
map_substrate.style.left = '0px';
map_substrate.style.top = '0px';
var downLeft, downTop; // where the substrate was when we started moving
var downX, downY; // where the mouse was on screen when we started moving
function movemaps(evt) {
if(!evt) var evt = window.event;
map_substrate.style.left = (downLeft + evt.screenX - downX) + 'px';
map_substrate.style.top = (downTop + evt.screenY - downY) + 'px';
};
// tell IE we aren't really dragging images around
map_vport.ondragstart = function(evt) { return false; };
map_vport.onmousedown = function(evt) {
if(!evt) var evt = window.event;
// don't harm right button
if((evt.which && evt.which == 3) || (evt.button && evt.button == 2)) return true;
downLeft = parseInt(map_substrate.style.left);
downTop = parseInt(map_substrate.style.top);
downX = evt.screenX;
downY = evt.screenY;
this.onmousemove = movemaps;
};
map_vport.onmouseup = function(evt) {
this.onmousemove = null;
// load the newly visible map data...
return true;
};
You can see this in action if you open a map page, "visit" javascript:map_con.style.overflow = 'visible'; void(1) to stop hiding the map squares outside the viewport, then pan around.
The part I did Thursday night was to load and display the newly exposed sims and landmarks using XMLHttpRequest. Currently I use DOM element construction to add things to the page, so I'll only summarize it here:
function updateData(xhr) {
if(!xhr.responseXML) return true;
var i;
var sims = xhr.responseXML.getElementsByTagName('sim');
for(i = 0; i < sims.length; i++) {
var sim = sims.item(i);
var simEl = newSim(sim); // see real code
// static numbers are the world coords for the original bottom and left edges of the viewport
simEl.style.bottom = parseInt((simEl.y - 257498) / 2) + 'px';
simEl.style.left = parseInt((simEl.x - 255668) / 2) + 'px';
map_substrate.appendChild(simEl);
}
var places = xhr.responseXML.getElementsByTagName('place');
for(i = 0; i < places.length; i++) {
var place = places.item(i);
var placeEl = newPlace(place); // see real code
// static numbers are the world coords for the original bottom and left edges of the viewport
placeEl.style.bottom = parseInt((placeEl.y - 257498) / 2) + 'px';
placeEl.style.left = parseInt((placeEl.x - 255668) / 2) + 'px';
map_substrate.appendChild(placeEl);
}
// stop throbber
map_throbber.style.visibility = 'hidden';
map_throbber.style.display = 'none';
}
map_vport.onmouseup = function(evt) {
this.onmousemove = null;
// skip work if we didn't actually move
if(downLeft == parseInt(map_substrate.style.left) &&
downTop == parseInt(map_substrate.style.top)) return true;
// start throbber
map_throbber.style.display = 'block';
map_throbber.style.visibility = 'visible';
// Find the center of the visible map in SL world coords.
// 1px == 2 SL meters
var dx = 2 * parseInt(map_substrate.style.left);
var dy = 2 * parseInt(map_substrate.style.top);
// The static numbers are the original location's world coords.
// SL world coords are from the *bottom* left, so add dy.
var action = 'xhr_update/x=' + (256180 - dx) + '/y=' + (257882 + dy);
var xhr;
if(window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if(window.ActiveXObject) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && xhr.status == 200)
updateData(xhr);
};
xhr.open('GET', '/lm/' + action, true);
xhr.send('');
return true;
};
You could fill out newSim and newPlace pretty slimly by building HTML on the server side and setting innerHTML directly; as David Hansson noted wrt Rails, though, that only hides the complication on the server instead of actually lessening it. I thought there was some objection to using innerHTML, even a solely academic one, other than the strangeness of it being easier to employ templating on the server side and HTML parsing on the client instead of programming it with JavaScript, but I can't find one. Read my real code on the map page if you're interested in doing it with DOM manipulation.
Speaking of server side code, the backend for all this map hoohah was built over the past few weeks. Landmarker provided a static map with buttons to scroll around, and still does if you're using a non-XMLHttpRequest browser or have JavaScript disabled. All the data, images, etc were already built; I only had to add the Ajax on top. On the server side that meant adding a new data-only XML version of a map page for the XHR to fetch and parse. That was easy with Landmarker, which is built using Movable Type as an application framework, with HTML::Template replaces with Template Toolkit.
So that's how I built an Ajax map. I haven't actually looked at the Google Maps code, so any hints or differences someone else might want to share are welcome.
Comments
comment
This is so unbelievably cool :) You’re crazy in the best ways!
comment
That is awesome! Well done!
comment
Mark your a geek and I’m proud to say that I went to school with you.
comment
Definitely awesome, you’re making me want to check Second Life out again. :)
comment
Holy crap!
comment
Very Cool, thanks for taking the time to document this.
comment
Bug/feature: 1. When you drag the map off the edge of the viewport and then release the mouse button, you stay in drag mode when your mouse returns to the map.
2. Would like to be able to have the mouse continue moving the map when in drag mode outside the viewport, releasing the mouse button would disable the drag mode.
Check out Google maps to see what I mean.
comment
Google Maps tiles the entire space, so what they have is just enough tiles to cover the viewport and a reasonable preload area around it, and then when you drag, they pick up tiles on one side and put them down on the other…
comment
Wow! Very impressive!
comment
The map is immpresiive.. i would like to know how can i communicate data with the map.i mean from a Mysql databse.
comment
I’ve been trying to implement such a map using ideas you discuss and some other pages but I can’t figure it out!
Is the substrate the full size of the world with only the visiable (and slightly off screen) cells loaded or do you load on demand, and if you do load on demand how does that work?
comment
The substrate is really kind of an anchor, relative to which you can position each map square.
The place data is loaded on demand (that’s what
updateDatais all about), but once it’s loaded, it’s not removed. As you browse around the world, more and more data is loaded. If you tried to browse the entire world with it, you’d see memory usage go up and up, perhaps crashing your browser .If you wanted to avoid that, you might keep an LRU list of map segments, and start losing the old ones as you load new ones beyond a threshold.