Sunday, February 04, 2007

Map Your Travels, 2.0

UPDATE: 07-15-2007 - Blogger made some more minor feed changes which broke this hack. This was a modification to the summary category feed, and specifically it was a change in the order of the links available per entry. Previously link entry number 1 pointed to a summary version of the post. Now link number 1 points to the comments.

To correct this, in the function storeFeed I had to modify this line:

var link = entry['link'][1].href;

to this:

var link = entry['link'][2].href;

UPDATE: 06-11-2007 - Blogger apparently made some minor changes to their feeds recently which changed the ordering of some feed items and broke this hack. Specifically, there's a function storeFeed which had the following lines:

if (i==0) {
var category = entry['category'][0].term;
var index = mapLevels.indexOf(category);
}

The index variable was returning a value of -1, meaning it did not exist. To get this to work again, I had to change the category to entry['category'][1].term. I have updated the code below.



Shortly before implementing the Travel Map hack, I heard that Blogger began offering JSON versions of their feeds. Blogger had long offered Atom feeds, but those could not be utilized without a feed reader or aggregator, requiring a set up with non-Blogger applications if you wanted to use your own feeds in your own blog.

JSON changes all of that, and being able to use my own Blogger feeds in my blog, the Travel Map hack can be vastly simplified and improved. I put it off for a while, but due to some odd behavior in the first version, I worked on and now unveil version 2 with AJAJ.

Microsoft Internet Explorer still can't handle this feature (v6 or v7), so the "places I've been" link below my profile remains disabled if you're using IE. But if you want to watch IE crash, here's a live link. And again, if anyone knows the nonstandard MS-HTML I need to use to prevent IE from crashing, let me know.

The way the posts need to be composed has changed slightly from version 1:

Post Titles

All post titles must consist of: higher-level post name, this-level post name, latitude, longitude, zoom level, and H S or M map type (Hybrid, Satellite, or Map).

All of these values must separated by commas, no spaces. Examples:

Map,world,35.704857,-6.926808,1,S
world,China,35.86166,104.195397,3,H
China,Great Wall,40.355595,116.009375,17,S
China,Wuhan,30.579997,114.269829,12,S
Wuhan,York Bar,30.589035,114.297032,18,S
How does one get the latitude and longitude? The easiest way is to go to Google Maps, find the place you're looking for, and click on "Link to this page." You'll see the page reload, and in the address bar, you'll see part that goes "&ll=" followed by two numbers:



This is the latitude and longitude, and you can copy them directly from there. Incidentally, the zoom level will be just before this, where it says "&z=".

For the world post, which is the introductory top-level post, the initial value in the title must be "Map", like so:
Map,world,35.704857,-6.926808,1,S

Post Labels

Each post should have two labels. Every post should have the label, atravelmap. Then, each post should have another label depending on what level it's on: countrymap, regionmap, citymap, or placemap. The atravelmap label is used to group all these posts together. The other labels are there to help with sorting the posts into the right hierarchy.

The CSS will prevent these labels from appearing in the labels widget.

Post Date

Before you publish the post, you need to backdate it. Usually I save the post as a draft first, then change the date and publish it. With the code I have above, the posts should be backdated to 1985 (I just arbitrarily chose this year). The code will prevent this year from appearing in the Blog Archive widget.

Also, the code will prevent any of the posts from appearing via the "Older" link. Note: if you don't have very many posts currently, you may need to modify your Blog Settings (Formatting) to only display a certain number of posts on your home page. This will prevent the travel posts, with their funky titles, from appearing on your homepage.

You will need to play with the months and days a bit to get them to show up alphabetically.

I also disable comments for my travel posts, and the code I have does not allow for comments to even be displayed for these posts, but that could be fairly easily changed.

Post Body

The post body has no special requirements. However, if you want a marker or markers to appear for a location, you must include a span with an id of "markers" in your post body. Within the span tags, you should include the latitude, longitude, and name of each marker. Each marker can appear on a separate line, like so:
<span id="markers">
39.930011,116.399975,Beijing
31.229999,121.470001,Shanghai
</span>Post text can begin here...
Note: This assumes the Settings -> Formatting -> Convert line breaks is set to Yes. In other words, when you hit Enter when composing a post, it will be replaced with a single <br /> tag in your blog. If you have this setting on No, then the markers should be entered as follows:

<span id="markers"><br />
39.930011,116.399975,Beijing<br />
31.229999,121.470001,Shanghai<br />
</span>Post text can begin here...

NEW! By request, you can now show routes, trails, or paths with some data included in your post body. For this functionality to work, you'll need to get a hex color, and two pieces of data from this Google utility. In particular, you will use this utility to draw your route, and then obtain the Encoded Polyline and the Encoded Levels that it generates.

In the body of your email, include this span:
<span id="routes">
#FF0000,10,BBBBBBB,gutyDu~oxTzFgI|DiFhE}FxEiGjFaHjEeG
#3333cc,5,BBBBBB,{avyDmcqxTzVYaJaIpMkS`G}JgMaE
</span>Post text can begin here...

You can have multiple routes, one route per line (no carriage returns in the middle of the route data!). The first bit of data will be your hex color, which of course will be the color of the line on the map. The second bit is the line width (I'm not sure what the ranges are). The third bit of data is the Encoded Levels that you get from the Google utility mentioned above, and the fourth bit of data is the Encoded Polyline.

Note: This assumes the Settings -> Formatting -> Convert line breaks is set to Yes. In other words, when you hit Enter when composing a post, it will be replaced with a single <br /> tag in your blog. If you have this setting on No, then the markers should be entered as follows:
<span id="routes"><br />
#FF0000,10,BBBBBBB,gutyDu~oxTzFgI|DiFhE}FxEiGjFaHjEeG<br />
#3333cc,5,BBBBBB,{avyDmcqxTzVYaJaIpMkS`G}JgMaE<br />
</span>Post text can begin here...

Also note that the Polylines are a little buggy. I noticed when I was experimenting with it that random markers would sometimes appear. You can see the one example (so far) on my map by clicking on China -> Wuhan -> Jianghan Road.

The code below has been updated to include this new functionality.

Here's how to add this functionality to your blog:

1. Go here to get a Google Maps API key for your blog. There's no limit on the number of URL's you can register for a key, so I'd highly recommend getting one for a test blog first.

2. Generate an animated loading graphic here, and download it to your machine.

3. Start a new post and upload the graphic you created in step 2. After it uploads, you'll see some HTML appear in your post. Copy the web address that is between the quotes where it says href="". Save this post as a draft (entitle it something like "Graphics"). The purpose of this post is only for allowing you to upload graphics, photos, etc for use on your blog, and should always remain in draft status.

4. In the Page Layout view, add three HTML/JavaScript widgets to your blog:
a. Map Control - put this in your sidebar. This widget will contain the links to the various map locations.
b. Google Map - put this one in the large main section, above the Blog Posts widget.
c. Map Link - put this wherever you want the link to your travel map to appear (probably somewhere near your profile widget).

5. Download your template, and open it in a text editor. Add the following to your style sheet between the tags: <b:skin><![CDATA[/* and ]]></b:skin> :

li#atravelmap, li#worldmap, li#countrymap, li#regionmap, li#citymap, li#placemap {
display:none;}

a.worldmap:link, a.countrymap:link, a.regionmap:link, a.citymap:link {
text-decoration:none; color:#3a80b4;}

#worlddiv, #countrydiv, #regiondiv, #citydiv {
margin-left: 5px;}

#countrydiv li, #regiondiv li, #citydiv li {
list-style-type:none; display:inline;}

#placediv ul {
margin-left:13px;}

#placediv li {
list-style-type: disc; text-align: left;}

#markers {
display:none;}

#routes {
display:none;}

6. Add the following scripts to the header element, between the tags: ]]></b:skin> and </head> :

<script type="text/javascript">
var priorEntry = "";
var count = 0;
var highlightCount = 0;

var mapLevels = new Array("worldmap","countrymap","regionmap","citymap","placemap");
var entryTitle = new Array();
var postFeedLink = new Array();
var postID = new Array();

function loadMapFeeds() {
priorEntry = "Map";
entryTitle.length = 0;
postFeedLink.length = 0;
postID.length = 0;
count = 1;
highlightCount = 0;

for (var i=0; i &lt; mapLevels.length; i++) {
document.getElementById("contentdiv").innerHTML = "<img alt='Loading...' src='link-to-your-animated-loading-gif' />";

var divID = mapLevels[i].replace(/map/,"div");
document.getElementById(divID).innerHTML = "";

if (document.getElementById("catFeed" + i)) {
var scriptid = document.getElementById("catFeed" + i);
scriptid.parentNode.removeChild(scriptid);
}

entryTitle[i] = new Array();
postFeedLink[i] = new Array();
postID[i] = new Array();

var feed_load = document.createElement('script');
var url = "/feeds/posts/summary/-/"+mapLevels[i]+"?alt=json-in-script&amp;callback=storeFeed&amp;max-results=200";
feed_load.src = url;
feed_load.type = "text/javascript";
feed_load.id = "catFeed" + i;
document.getElementsByTagName('head')[0].appendChild(feed_load);
}
}

function storeFeed(root) {
var feed = root.feed;

for (var i = 0; i &lt; feed.entry.length; i++) {
var entry = feed.entry[i];

if (i==0) {
var category = entry['category'][1].term;
var index = mapLevels.indexOf(category);
}

var title = entry.title.$t;
var link = entry['link'][2].href;
var post_id = entry.id.$t;

entryTitle[index].push(title);
postFeedLink[index].push(link);
postID[index].push(post_id);

if(i==(feed.entry.length-1)) {
count++
}
}

if (count==mapLevels.length) {
showTitles(0);
}
}

function showTitles(level) {
var html = ['&lt;ul&gt;'];
for (var i = 0; i &lt; entryTitle[level].length; i++) {
var splitTitle = entryTitle[level][i].split(",");
if (splitTitle[0]==priorEntry) {
var aposTitle = splitTitle[1].replace(/'/,"apostrophe");
var nextEntry = '\"' +aposTitle+ '\"';
var t = getType(splitTitle[5]);
var link = postFeedLink[level][i].replace(/summary/,"full");
var quoteLink = '\"' + link + '\"';
var splitPostID = postID[level][i].split("-");
quotePostID = '\"' +splitPostID[2]+ '\"';
var category = '\"' + mapLevels[level] + '\"';
if (mapLevels[level]=="placemap") {
html.push("<li><a href='javascript:void(0);' onclick='javascript:showContent(",quoteLink,"); map.clearOverlays(); map.setCenter(new GLatLng(",splitTitle[2],",",splitTitle[3],"),",splitTitle[4],",",t,")'>", splitTitle[1], "</a>", "</li>");
}
else {
html.push(" <li><a class='",mapLevels[level],"' id='",splitPostID[2],"' href='javascript:void(0);' onMouseOver = 'javascript:highlightmap(" ,quotePostID, ")' onMouseOut='javascript:unhighlightmap(" ,quotePostID, ")' onclick='javascript:showContent(",quoteLink,"); showNext(",nextEntry,",",category,",",quotePostID,"); map.clearOverlays(); map.setCenter(new GLatLng(",splitTitle[2],",",splitTitle[3],"),",splitTitle[4],",",t,")'>", splitTitle[1], "</a>", "</li> ");
}
}
}

var whatdiv = mapLevels[level].replace(/map/,"div");
document.getElementById(whatdiv).innerHTML = html.join("");
if (mapLevels[level]=="worldmap" &amp;&amp; count==mapLevels.length) {
showContent(link);
showNext(aposTitle,mapLevels[level],splitPostID[2]);
count = 0;
}
}

function showNext(nextEntry,category,thisPostID) {
nextEntry = nextEntry.replace(/apostrophe/,"\'");
priorEntry = nextEntry;

var allCategoryClass = getElementsByClass(category);
for(var y=0;y &lt; allCategoryClass.length;y++) {
allCategoryClass[y].style.backgroundColor="#efefef";
allCategoryClass[y].style.color="#387fb5";
}

document.getElementById(thisPostID).style.backgroundColor="#c0c0c0";
document.getElementById(thisPostID).style.color="#000000";
highlightCount++;

var index = mapLevels.indexOf(category);
for (i=index+1; i &lt; mapLevels.length; i++) {
showTitles(i);
}

}

function showContent(postFeedLink) {
document.getElementById("contentdiv").innerHTML = "<img alt='Loading...' src='link-to-your-animated-loading-gif' />";

var content_load=document.createElement('script');
var url=postFeedLink + "?alt=json-in-script&amp;callback=showfeedcontent";
content_load.src=url;
content_load.type="text/javascript";
document.getElementsByTagName('head')[0].appendChild(content_load);
}

function showfeedcontent(root) {
document.getElementById("contentdiv").innerHTML = "";

var entry = root.entry;
var html = ['&lt;p&gt;'];
var content = entry.content.$t;
html.push(content);
document.getElementById("contentdiv").innerHTML = html.join("");

if (document.getElementById("markers")) {
var markers = document.getElementById("markers").innerHTML;
markers = markers.replace(/&lt;br \/&gt;/g,"|");
markers = markers.replace(/&lt;br&gt;/g,"|");
markers = markers.split("|");
var markernum = markers.length;
for (i = 0; i &lt; markernum; i++) {
if (markers[i]!="") {
var onemarker = markers[i].split(",");
var description = onemarker[2]
var point = new GLatLng(onemarker[0],onemarker[1]);
var marker = createMarker(point,description);
map.addOverlay(marker);
}
}
}

if (document.getElementById("routes")) {
var routes = document.getElementById("routes").innerHTML;
routes = routes.replace(/<br \/>/g,"¦");
routes = routes.replace(/<br>/g,"¦");
routes = routes.split("¦");
var routenum = routes.length;
for (i = 0; i < routenum; i++) {
if (routes[i]!="") {
var oneroute = routes[i].split(",");
var encodedPolyline = new GPolyline.fromEncoded({
color: oneroute[0],
weight: oneroute[1],
points: oneroute[3],
levels: oneroute[2],
zoomFactor: 32,
numLevels: 4
});
map.addOverlay(encodedPolyline);
}
}
}
}

function highlightmap (a) {
if (document.getElementById(a).style.backgroundColor=="rgb(192, 192, 192)") {
highlightCount++;
}
else {
document.getElementById(a).style.backgroundColor="#c0c0c0";
document.getElementById(a).style.color="#000000";
highlightCount=0;
}
}

function unhighlightmap (a) {
if (highlightCount == 0) {
document.getElementById(a).style.backgroundColor="#efefef";
document.getElementById(a).style.color="#387fb5";
}
}

// Thanks to Mike at http://www.econym.demon.co.uk/ for his tutorial, which included this bit:
function createMarker(point,html) {
var marker = new GMarker(point);
GEvent.addListener(marker, "click", function() {
marker.openInfoWindowHtml(html);
});
return marker;
}

function getType (a) {
var typeparameter ="";
var maptype = a;
if (maptype == "H") {
typeparameter = "G_HYBRID_MAP"
}
else if (maptype == "S") {
typeparameter = "G_SATELLITE_MAP"
}
else if (maptype == "M") {
typeparameter = "G_NORMAL_MAP"
}
return typeparameter;
}

function delay(a,b) {
map.panTo(new GLatLng(a,b));
}

// Thanks to Dustin Diaz at http://www.dustindiaz.com/ for this great Javascript function
function getElementsByClass(searchClass,node,tag) {
var classElements = new Array();
if ( node == null )
node = document;
if ( tag == null )
tag = '*';
var els = node.getElementsByTagName(tag);
var elsLen = els.length;
var pattern = new RegExp("(^|\\s)"+searchClass+"(\\s|$)");
for (i = 0, j = 0; i &lt; elsLen; i++) {
if ( pattern.test(els[i].className) ) {
classElements[j] = els[i];
j++;
}
}
return classElements;
}
</script>


7. Modify the three widgets you added previously as follows:

a.

<b:widget id='HTMLa' locked='false' title='Map Controls' type='HTML'>
<b:includable id='main'>
Add this <b:if cond='data:blog.url == "http://your-blog-name.blogspot.com/search/label/atravelmap"'>
Remove this <!-- only display title if it's non-empty -->
Remove this <b:if cond='data:title != ""'>
Remove this <h2 class='title'><data:title/></h2>
Remove this </b:if>
<div class='widget-content'>
<data:content/>
</div>
<b:include name='quickedit'/>
</b:if>
</b:includable>
</b:widget>


b.

<b:widget id='HTMLb' locked='false' title='Google Map' type='HTML'>
<b:includable id='main'>
Add this <b:if cond='data:blog.url == "http://your-blog-name.blogspot.com/search/label/atravelmap"'>
Remove this <!-- only display title if it's non-empty -->
Remove this <b:if cond='data:title != ""'>
Remove this <h2 class='title'><data:title/></h2>
Remove this </b:if>
<div class='widget-content'>
<data:content/>
</div>
<b:include name='quickedit'/>
</b:if>
</b:includable>
</b:widget>


c.

<b:widget id='HTMLc' locked='false' title='Map Link' type='HTML'>
<b:includable id='main'>
Add this <b:if cond='data:blog.url == "http://your-blog-name.blogspot.com/search/label/atravelmap"'>
Remove this <!-- only display title if it's non-empty -->
Remove this <b:if cond='data:title != ""'>
Remove this <h2 class='title'><data:title/></h2>
Remove this </b:if>
<div class='widget-content'>
<data:content/>
</div>
<b:include name='quickedit'/>
</b:if>
</b:includable>
</b:widget>


8. Find the Blog Posts widget:

<b:widget id='Blog1' locked='false' title='Blog Posts' type='Blog'>

Then find the main includable, and the div with an id of "blog-posts":

<b:includable id='main' var='top'>
<div id='blog-posts'>

Directly below the div element, add the following:

<b:if cond='data:blog.url == "http://your-blog-name.blogspot.com/search/label/atravelmap"'>
<div id="contentdiv">
</div>
<b:else />

Just before the closing div tag for the "blog-posts" div, insert the closing if tag, like so:

</b:if>
</div>


9. Add the following conditional tags around the "nextprev" include, and the "feedLinks" include, like so:

<b:if cond='data:blog.url != "http://your-blog-name.blogspot.com/search/label/atravelmap"'>
<!-- navigation -->
<b:include name='nextprev'/>

<!-- feed links -->
<b:include name='feedLinks'/>
</b:if>


10. In the "nextprev" includable, find the olderPageUrl conditional statement, and modify the href attribute so it includes the bolded text:

<b:if cond='data:olderPageUrl'>
<a expr:href='data:olderPageUrl + "&amp;updated-min=2000-08-31T00%3A00%3A00-00%3A00"' expr:title='data:olderPageTitle' id='blog-pager-older-link'><data:olderPageTitle/></a>
</b:if>

11. Find the label widget:

<b:widget id='Label1' locked='false' title='Labels' type='Label'>

In the data:labels loop, modify the <li> element so it includes the bolded text:

<b:loop values='data:labels' var='label'>
<li expr:id='data:label.name'>
<b:if cond='data:blog.url == data:label.url'>
<data:label.name/>
<b:else/>
<a expr:href='data:label.url'><data:label.name/></a>
</b:if>
(<data:label.count/>)
</li>
</b:loop>


12. Find the blog archive widget:

<b:widget id='BlogArchive1' locked='false' title='Blog Archive' type='BlogArchive'>

Assuming you are using the hierarchy view for your archive, find the "interval" includable, and add the conditional tags as follows:

<b:includable id='interval' var='intervalData'>
<b:loop values='data:intervalData' var='i'>
<b:if cond='data:i.name != "1985"'>
<ul>
<li expr:class='"archivedate " + data:i.expclass'>
<b:include data='i' name='toggle'/>
<a class='post-count-link' expr:href='data:i.url'><data:i.name/></a>
(<span class='post-count'><data:i.post-count/></span>)
<b:if cond='data:i.data'>
<b:include data='i.data' name='interval'/>
</b:if>
<b:if cond='data:i.posts'>
<b:include data='i.posts' name='posts'/>
</b:if>
</li>
</ul>
</b:if>
</b:loop>
</b:includable>

If you're using the flat view or drop-down menu, I'm not sure how to easily block out an entire year.

13. Add the following to the very last widget on your template (for me, this is the Profile widget). This should be inside the main includable:

<b:includable id='main'>


<b:if cond='data:blog.url == "http://your-blog-name.blogspot.com/search/label/atravelmap"'>
<script type="text/javascript">
setTimeout("loadMapFeeds()",200);
</script>
</b:if>


14. Save the template changes, and upload the template to Blogger. Go to the Page Layout view, and add the following data to your HTML widgets:

a. Map Control

<a href="http://your-blog-name.blogspot.com/">back home</a>
<p>
<ul>
<li>
<div id="worlddiv">
</div>
</li>
<li><span class="travelsection">country:</span>
<div id="countrydiv">
</div>
</li>
<li><span class="travelsection">region:</span>
<div id="regiondiv">
</div>
</li>
<li><span class="travelsection">city:</span>
<div id="citydiv">
</div>
</li>
<li><span class="travelsection">place:</span>
<div id="placediv">


</div>
</li>
</ul>
</p>


b. Google Maps

<script src="http://maps.google.com/maps?file=api&v=2&key=YOUR-GOOGLE-MAP-KEY" type="text/javascript">
</script>

<div id="map" style="width: 100%; height: 400px"></div>

<noscript>JavaScript must be enabled for Google Maps. JavaScript is either disabled or not supported by your browser.
</noscript>

<script type="text/javascript">
if (GBrowserIsCompatible()) {
// Display the map, with some controls and set the initial location
var map = new GMap2(document.getElementById("map"));
map.addControl(new GLargeMapControl());
map.addControl(new GMapTypeControl());
map.setCenter(new GLatLng(35.704857,-6.926808),1,G_SATELLITE_MAP);
map.enableContinuousZoom();
}
else {
alert("The Google Maps API is not compatible with this browser");
}
</script>


c. Map Link

<!--[if IE]>
<script type="text/javascript">
function iealert() {
alert("Internet Explorer does not support this function. Get the better browser. Get Firefox at http://www.mozilla.com");
}
</script>
<p>
<a id="travellink" href="javascript:void(0);" onclick="javascript:iealert();">places I've been</a>
</p>
<![endif]-->

<![if !ie]>
<p>
<a id="travellink" href="http://your-blog-name.blogspot.com/search/label/atravelmap">places I've been</a>
</p>
<![endif]></![endif]></![if>


Now you're all set. There's room for improvement, I know, and I'm eventually hoping to add some more functionality. If you see anything strange, or anything that I missed, or if you see something that is needlessly complex, leave a comment and let me know.A much improved Blogger Beta hack utilizing AJAJ that integrates Google Maps with your blog, and allows you to show the locations of your travels.