LJ Friendsmap Gode

as of 9 July 2007

Python Data Collector

#LJ Friends Map Python module #(c) 2005 Jon Evans #hereby released under terms of the GNU General Public License, version 2 import urllib import os.path import sys import time import xml.dom.minidom #Constants xmlHeaderString = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" #Entry-point methods def generate(username, forceRegenerate=0): # convenience to save typing return generateFriendsMapXML(username, forceRegenerate) def generateFriendsMapXML(username, forceRegenerate=0): # check input try : if len(username) == 0: raise UsernameError, "Empty string passed as username" elif len(username) > 256: raise UsernameError, "Excessive-length username "+username+"." else: rootUser = LJUser(username) rootUser.findLocation(forceRegenerate) friends = getUserRelations(username) for friend in friends: print "<BR>Parsing "+friend.name friend.findLocation(forceRegenerate) filename = writeMappableXML(rootUser, friends) except: logError(username, sys.exc_info()[0]) raise; return filename #Heavy resource hog, to be used with caution def generateFriendsMapXMLForAllFriendsOf(username): try: rootFriends = getUserRelations(username) for rootFriend in rootFriends: generateFriendsMapXML(rootFriend.name) except: logError("friendsfriends error for "+username, sys.exc_info()[0]) raise; #Classes #Can't seem to find FancyURLopener, alas. class LJScrapeURLOpener(urllib.FancyURLopener): def __init__(self, *args): self.version = "rezendi.zigamorph.net/lj-friendsmap.htm: jonemevans@yahoo.com" apply(urllib.FancyURLopener.__init__, (self,) + args) class LJLocation: def __init__(self, locname): self.locationName=locname self.latitude="unknown" self.longitude="unknown" self.asof="" self.locationName=self.locationName.replace("&","|") self.locationName=self.locationName.replace("/","|") def __str__(self): output = "" try: output = output+str(self.locationName) output = output+": "+str(self.latitude) output = output+": "+str(self.longitude) output = output+": "+str(self.asof) except: output = output+"---Error printing: "+str(sys.exc_info()[0]) return output #XML methods def getXMLPath(self): xmlFileName=self.locationName+"-location.xml" if os.path.isdir("lj-location-xml"): xmlFileName=os.path.normpath("lj-location-xml/"+xmlFileName) return xmlFileName; def toXML(self): xml="<location as-of=\""+str(time.time())+"\">" xml=xml+"<location-name>"+self.locationName+"</location-name>" xml=xml+"<latitude>"+self.latitude+"</latitude>" xml=xml+"<longitude>"+self.longitude+"</longitude>" xml=xml+"</location>" return xml def writeXML(self): try: xmlFileName=self.getXMLPath() xmlToWrite=xmlHeaderString+self.toXML() xmlFile=open(xmlFileName, "w") xmlFile.write(xmlToWrite.encode("utf-8")) xmlFile.close() except: logError("Error writing XML for "+self.locationName, sys.exc_info()[0]) #Application methods def isLatLongUnknown(self): if (self.latitude=="unknown"): return 1 if (self.longitude=="unknown"): return 1 return 0 def findLatitudeLongitude(self, forceRegenerate=0): #First of all, see if location data is locally cached xmlFileName=self.getXMLPath().encode("utf-8") if os.path.exists(xmlFileName): self.loadFromFile(xmlFileName) if forceRegenerate!=0 or isStaleOrInvalidData(self.asof, "latitude-longitude"): try : self.loadFromGoogle() if (self.isLatLongUnknown()): self.loadFromMultimap() except: logError("Error finding latitude/longitude for "+self.locationName, sys.exc_info()[0]) self.writeXML() def loadFromFile(self, xmlFileName): try: addXMLHeaderIfNecessary(xmlFileName) dom=xml.dom.minidom.parse(xmlFileName) locations=dom.getElementsByTagName("location") self.asof = locations[0].getAttribute("as-of") latitudes = dom.getElementsByTagName("latitude") self.latitude = latitudes[0].firstChild.nodeValue longitudes = dom.getElementsByTagName("longitude") self.longitude = longitudes[0].firstChild.nodeValue dom.unlink() except: logError("Error loading lat-long from file for "+self.locationName, sys.exc_info()[0]) def loadFromGoogle(self): queryString = self.locationName queryString = queryString.replace(" ","+") #www.google.com latLongPageURL = "http://72.14.207.99/maps?q="+queryString latLongPageSource=getHTMLPage(latLongPageURL) #Not that getting a substring should be a function or anything... markerHeadText = "<point lat=" markerHeadLocation = latLongPageSource.find(markerHeadText) #if location not found, just return if (markerHeadLocation == -1): return latLongPageSource = latLongPageSource[markerHeadLocation:] markerFootText = ">" markerFootLocation=latLongPageSource.find(markerFootText) if (markerFootLocation == -1): #highly unlikely this will ever happen raise GeocodeError, "Error finding lat-long in "+latLongPageSource latLongPageSource = latLongPageSource[:markerFootLocation] #Another split hack, this one even worse latLongList=latLongPageSource.split("\"") if len(latLongList) < 4: raise GeocodeError, "Unexpected latitude-longitude text "+str(latLongList) self.latitude=latLongList[1] self.longitude=latLongList[3] def loadFromMultimap(self): #If we've only got city/country or state/country, Multimap is #currentlyunreliable: give up immediately if (self.locationName.count(",") < 2): return queryString = self.locationName.replace(" ","+") #www.multimap.com latLongPageURL = "http://146.101.143.10/map/places.cgi?client=public&db=w3&place="+queryString latLongPageSource=getHTMLPage(latLongPageURL) #Usual screen scraper problem: vulnerable to HTML format changes if (latLongPageSource.find("lon=") == -1): return if (latLongPageSource.find("lat=") == -1): return #If there are no matches, mark it as unknown if (latLongPageSource.find("no exact matches") > 0): return if (latLongPageSource.find("no matching places") > 0): return #If there's only one result, go get it markerHeadLocation=latLongPageSource.find("geo.position") latLongPageSource=latLongPageSource[markerHeadLocation:] markerHeadLocation=latLongPageSource.find("content")+9 markerFootLocation=latLongPageSource.find("\" />", markerHeadLocation) geoPosition=latLongPageSource[markerHeadLocation:markerFootLocation] if geoPosition!="0;0": geoPositions=geoPosition.split(";") self.latitude=geoPositions[0] self.longitude=geoPositions[1] return #If we're here, we have multiple results #First of all, make sure the country matches, because Multimap #does some pretty weird things. selfCountryName=self.locationName[self.locationName.rfind(",")+1:] #Get Multimap's country idea #zoom down to results list #Incredibly messy markerHeadLocation = latLongPageSource.find("browse.cgi") latLongPageSource=latLongPageSource[markerHeadLocation:] markerFootLocation = latLongPageSource.find("</a>") countryHrefChunk= latLongPageSource[:markerFootLocation] countryChunkTwo=countryHrefChunk[countryHrefChunk.rfind(",")+2:] mmCountry=countryChunkTwo[:countryChunkTwo.rfind("\"")] #If one country includes the other, OK; otherwise no if (selfCountryName.find(mmCountry)==-1): if (mmCountry.find(selfCountryName)==-1): return markerHeadLocation = latLongPageSource.find("lon=") #if location not found, just return if (markerHeadLocation == -1): return markerFootLocation=latLongPageSource.find("&", markerHeadLocation) if (markerFootLocation == -1): #highly unlikely this will ever happen raise GeocodeError, "Error finding longitude in "+latLongPageSource self.longitude = latLongPageSource[4+markerHeadLocation:markerFootLocation] markerHeadLocation = latLongPageSource.find("lat=") #if location not found, just return if (markerHeadLocation == -1): return markerFootLocation=latLongPageSource.find("&", markerHeadLocation) if (markerFootLocation == -1): #highly unlikely this will ever happen raise GeocodeError, "Error finding latitude in "+latLongPageSource self.latitude = latLongPageSource[4+markerHeadLocation:markerFootLocation] class LJUser: def __init__(self, username): self.name=username self.isFriend="False" self.isFriendedBy="False" self.location=None self.asof="" def __str__(self): output = "" output = output+self.name if self.isFriend: output = output+": Friend " else: output = output+": NotFriend" if self.isFriendedBy: output = output+": FriendedBy " else: output = output+": NotFriendedBy" output = output+": "+str(self.location) output = output+": "+str(self.asof) return output #XML methods def getUserLocationXMLPath(self): xmlFileName=self.name+"-location.xml" if os.path.isdir("lj-user-location-xml"): xmlFileName=os.path.normpath("lj-user-location-xml/"+xmlFileName) return xmlFileName; def toUserLocationXML(self): xml="" xml=xml+"<lj-userlocation as-of=\""+str(time.time())+"\">" xml=xml+"<username>"+self.name+"</username>" xml=xml+"<location-name>"+self.location.locationName+"</location-name>" xml=xml+"</lj-userlocation>" return xml def writeUserLocationXML(self): try: xmlFileName=self.getUserLocationXMLPath() xmlToWrite=xmlHeaderString+self.toUserLocationXML() xmlFile=open(xmlFileName, "w") xmlFile.write(xmlToWrite.encode("utf-8")) xmlFile.close() except: logError("Error writing user location XML for "+self.name, sys.exc_info()[0]) def toMappableXML(self): xml="" xml=xml+"<lj-user>" xml=xml+"<username>"+self.name+"</username>" xml=xml+"<friended>"+str(self.isFriend)+"</friended>" xml=xml+"<friendedby>"+str(self.isFriendedBy)+"</friendedby>" if (self.location!=None): xml=xml+self.location.toXML(); xml=xml+"</lj-user>\n" return xml #Application methods def findLocation(self, forceRegenerate=0): #First of all, see if location data is locally cached xmlFileName=self.getUserLocationXMLPath() if os.path.exists(xmlFileName): self.location = self.getUserLocationFromFile(xmlFileName) if forceRegenerate!=0 or isStaleOrInvalidData(self.asof, "user location"): self.location = self.getUserLocationFromLJ() self.writeUserLocationXML() self.location.findLatitudeLongitude(forceRegenerate) def getUserLocationFromFile(self, xmlFileName): locationName = "" #filtering all complex characters try: addXMLHeaderIfNecessary(xmlFileName) dom=xml.dom.minidom.parse(xmlFileName) userLocations=dom.getElementsByTagName("lj-userlocation") self.asof = userLocations[0].getAttribute("as-of") locationNames=dom.getElementsByTagName("location-name") locationName = locationNames[0].firstChild.nodeValue dom.unlink() except: logError("Error getting user location from file for "+self.name, sys.exc_info()[0]) locationName = "blank" return LJLocation(locationName) def getUserLocationFromLJ(self): #www.livejournal.com infoPageURL = "http://66.150.15.150/userinfo.bml?user="+self.name infoPageSource=getHTMLPage(infoPageURL) #get the location markerHeadText = "<tr><td align='right'><b>Location:</b>" markerHeadLocation = infoPageSource.find(markerHeadText) #if no location keyword found, just return if (markerHeadLocation == -1): return LJLocation("blank") infoPageSource = infoPageSource[markerHeadLocation:] markerFootText = "</tr>" markerFootLocation=infoPageSource.find(markerFootText) if (markerFootLocation == -1): #highly unlikely this will ever happen raise InfopageError, "Unable to find end of location section" infoPageSource = infoPageSource[:markerFootLocation] #use LJ's <a href> links to isolate the location names #kind of ass-backwards and kludged, but hey locationList=infoPageSource.split("<a") #dump HTML-garbage first entry locationList.pop(0) #by the nature of HTML, this is bound to work #granted, it's a hack locationName = "" for entry in locationList: locationValue = entry[entry.find(">")+1:entry.find("<")] locationName = locationName + locationValue locationName = locationName +"," #Clean, strip the last comma locationName.strip() #if all we've got is a comma, make it blank if len(locationName)==1: locationName = "blank" elif len(locationName)==0: locationName = "blank" else: locationName = locationName[:-1] return LJLocation(unicode(locationName,"utf-8",'replace')) class LJFMError(Exception): #Base exception class pass class UsernameError(LJFMError): pass class InfopageError(LJFMError): pass class GeocodeError(LJFMError): pass #Utility methods def isStaleOrInvalidData(asOf, dataType): if len(asOf) == 0: return 1 dateWritten = float(asOf) ageOfData = time.time() - dateWritten if ageOfData>(30*24*60*60): return 1 return 0 def getHTMLPage(urlString): urlOpener = LJScrapeURLOpener() # print "Getting HTML page for "+urlString htmlPage = urlOpener.open(urlString.encode("utf-8")) htmlPageSource = htmlPage.read() return htmlPageSource def addXMLHeaderIfNecessary(xmlFileName): xmlFile=open(xmlFileName, "rw") firstLine=xmlFile.readline() if (firstLine.find("encoding")==-1): xmlFile.open(xmlFileName, "rw") wholeFile=xmlFile.read(); newFile=xmlHeaderLine+wholeFile; xmlFile.write(newFile.encode("utf-8")) xmlFile.close(); def getUserRelations(username): friendInfoPageURL = "http://66.150.15.150/misc/fdata.bml?user="+username friendInfoPageSource = getHTMLPage(friendInfoPageURL) if (friendInfoPageSource.find("! not a person account")>=0): return getCommunityUserData(username) friends = [] friendInfoLines=friendInfoPageSource.split("\n") friendInfoLines.sort() for infoLine in friendInfoLines: if len(infoLine)==0: continue if infoLine[0]!=">": if infoLine[0]!="<": continue friendName = infoLine[2:] #O(n^2)! OK for smallish friendslists, but should really fix #by sorting by friendname, then creating new friend object #just before friendname changes friend = getExistingEntry(friends, friendName) if (friend == None): friend = LJUser(friendName) friends.append(friend) if infoLine[0]==">": friend.isFriend="True" else: friend.isFriendedBy="True" return friends def getCommunityUserData(username): infoPageURL = "http://community.livejournal.com/"+userName+"/profile" infoPageURL = infoPageURL+"&mode=full" urlOpener = LJScrapeURLOpener() infoPage = urlOpener.open(infoPageURL) infoPageSource = infoPage.read() # Excise 'Friends' list # Find start of list, excise everything before markerHeadText = "<tr><td align='right' valign='top'><b><a href='" markerHeadText = markerHeadText+"http://community.livejournal.com/" markerHeadText = markerHeadText+username markerHeadText = markerHeadText+"/friends'>Members</a>:</b></td>" markerHeadLocation = infoPageSource.find(markerHeadText) if (markerHeadLocation == -1): raise InfopageError, "Unable to find friendslist on info page for "+username infoPageSource = infoPageSource[markerHeadLocation:] # Now that we've truncated to head of current list, pass it to the processor # with the name of the list and the end-of-list marker markerFootText = "</tr>" friends = [] processRawFriendText(friends, infoPageSource, "Members" , markerFootText); # Get 'Watched by' or 'Friend Of' or 'Also Friend of' list, if any markerHeadText = "Watched by:</b>" markerHeadLocation = infoPageSource.find(markerHeadText) if (markerHeadLocation >= 0): markerFootText = "</tr>" pageSource = infoPageSource[markerHeadLocation:] processRawFriendText(friends, infoPageSource, "Watched by", markerFootText); #all done return friends # Given an existing list of friends, some HTML source, a list name, a marker # for the end of the source, and a delimeter, add to and/or correctly modify # the friends list as per the new source data. def processRawFriendText(friends, friendText, listName, markerFootText, delimeter=">", delimeter2="<"): markerFootLocation = friendText.find(markerFootText) if (markerFootLocation == -1): raise InfopageError, "Unable to find end of "+listName +" list on info page, looking for "+markerFootText+ " in text "+friendText friendText=friendText[:markerFootLocation] initialList=friendText.split(".livejournal.com/profile") # remove first entry, which is HTML garbage initialList.pop(0) for entry in initialList: entryEnd=entry.find(delimeter)+1 entryEnd=entry.find(delimeter2) friendName = entry[entryStart:entryEnd] #Find existing entry for this named user, if any #O(n^2) - should clean up friend = getExistingEntry(friends, friendName) if (friend == None): friend = LJUser(friendName) friends.append(friend) if listName == "Members": friend.isFriend="True" elif listName == "Watched by": friend.isFriendedBy="True" return friends def getExistingEntry(friends, friendName): for friend in friends: if friend.name == friendName: return friend; return None; def writeMappableXML(rootUser, friends): #Create and open file xmlFileName=rootUser.name+".xml" if os.path.isdir("mappable-xml"): xmlFileName=os.path.normpath("mappable-xml/"+xmlFileName) xmlFile=open(xmlFileName, "w") xmlFile.write(xmlHeaderString) xmlFile.write("<ljfriendsmap>") #Write root user xmlFile.write("<root-user>") xmlFile.write(rootUser.toMappableXML().encode("utf-8")) xmlFile.write("</root-user>") #Write all friendings xmlFile.write("<lj-friendings as-of=\""+time.asctime()+"\">") for friend in friends: try: xmlFile.write(friend.toMappableXML().encode("utf-8")) except: logError("Error writing final XML for "+friend.name, sys.exc_info()[0]) xmlFile.write("</lj-friendings>") #Close file and quit xmlFile.write("\n</ljfriendsmap>") xmlFile.close() return 1 def logError(username, errorMessage): try: print "<BR>Non-fatal error: "+username+", message "+str(errorMessage) xmlFileName=username+"-"+str(time.time())+".xml" if os.path.isdir("error"): xmlFileName=os.path.normpath("error/"+xmlFileName) xmlFile=open(xmlFileName, "w") xmlToWrite=xmlHeaderString+str(errorMessage) xmlToWrite=unicode(xmlToWrite,"utf-8") xmlFile.write(xmlToWrite.encode("utf-8")) xmlFile.close() except: # give up pass #TODO: # XML Generation: # Fix O(n^2) processing # # Map: # Intelligent error message if file not found # UK parsing # More clustering/scaling # Fix O(n^2) processing # Friends/friends-of different colours # # Other: # Check regenerate box if coming from regenerate page </PLAINTEXT> <P><B>intermediate.cgi CGI Python Script</B></P> <PLAINTEXT> #!/usr/bin/python print "Content-type: text/html" print print "<html><head><title>Collecting friendsmap data...</title>" from cgi import escape import os userName="nobody" forceRegenerate=0 keys = os.environ.keys() keys.sort() for k in keys: if (escape(k)=="QUERY_STRING"): queryString=escape(os.environ[k]) userNameLoc=queryString.find("userName=") if (userNameLoc>=0): userName=queryString[userNameLoc+9:] forceRegenerateLoc=queryString.find("forceRegenerate=on") if (forceRegenerateLoc>=0): forceRegenerate=1 print "<meta http-equiv=\"refresh\" content=\"0; URL=collectData.cgi" if (forceRegenerate==1): print "?forceRegenerate=on&userName="+userName+"\">\n" else: print "?userName="+userName+"\">\n" print "</head><body>\n" print "<b>Collecting friendsmap data for "+userName+"</b><P>\n" print "<P>Kick back and have a beer..." print "</body></html>" </PLAINTEXT> <P><B>collectData.cgi CGI Python Script</B></P> <PLAINTEXT> #!/usr/bin/python print "Content-type: text/html" print print "<html><head><title>Friendsmap XML Generator</title></head><body>" import os, sys, ljfm9 from cgi import escape userName="nobody" forceRegenerate=0 keys = os.environ.keys() keys.sort() for k in keys: if (escape(k)=="QUERY_STRING"): queryString=escape(os.environ[k]) userNameLoc=queryString.find("userName=") if (userNameLoc>=0): userName=queryString[userNameLoc+9:] forceRegenerateLoc=queryString.find("forceRegenerate=on") if (forceRegenerateLoc>=0): forceRegenerate=1 print "<b>Collecting friendsmap data for "+userName+"</b><P>\n" ljfm9.generate(userName, forceRegenerate) print "<P><b>Finished!</b>\n" print "<P>Now go on to <a href=\"friendsmap.htm?userName="+userName+"\">view "+userName+"'s friendsmap</a>.\n" print "</body></html>" </PLAINTEXT> <P><B>friendsmap.htm Google Maps API HTML/JavaScript</B></P> <PLAINTEXT> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>LJ Friendsmap</title> <script src="http://maps.google.com/maps?file=api&v=1&key=ABQIAAAAnN8trvwy54S4_TFoEaPJxxSK9oBAXYlkHyyidtxN3dGTHIoFTxTFRq4MfC9hPiS2i7XIaj-U72ZgPA" type="text/javascript"></script> </head> <body onload="onLoad()"> <table width=675 cellpadding=0 cellspacing=0><tr><td align='left'><font size="-1">LJ Friendsmap, created by</font> <script type="text/javascript"> function getLJHTMLFor(usernameString) { return "<span class='ljuser' style='white-space: nowrap;'><a href='http://www.livejournal.com/userinfo.bml?user="+usernameString+"'><img src='http://stat.livejournal.com/img/userinfo.gif' alt='[info]' width='17' height='17' style='vertical-align: bottom; border: 0;' /></a><a href='http://www.livejournal.com/users/"+usernameString+"/'><b>"+usernameString+"</b></a></span>" } document.write(getLJHTMLFor("rezendi")); </script> <font size="-1">&nbsp;&nbsp;&nbsp;&nbsp;(Beta release. <a href="/">About, disclaimers, help, make a new map</a>)</font></td> </tr><tr><td align='right'> <script type="text/javascript"> //Taken from http://www.activsoftware.com/code_samples/code.cfm/CodeID/59/ function getQueryVariable(variable) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i=0;i<vars.length;i++) { var pair = vars[i].split("="); if (pair[0] == variable) { return pair[1]; } } } var userName=getQueryVariable("userName"); if (userName == undefined) { userName = "nobody"; } document.write("<font size='-1'>Click to see "+getLJHTMLFor(userName)+"'s friendings:"); </script> <a href="javascript:doAll()">All</a> <a href="javascript:doFriends()">Friends</a> <a href="javascript:doFriendedBy()">Friended By</a> <a href="javascript:doMutual()">Mutual</a> <a href="javascript:doStalking()">Stalking</a> <a href="javascript:doStalkedBy()">Stalked By</a><font size="-2">(kidding...)</font> <a href="javascript:doUnknown()">Unknown</a> <a href="javascript:doUnfound()">Unfound</a></font></td></tr></table> <table><tr><td colspan=2><div id="map" style="width: 675px; height: 540px"></div></td> </td> <script type="text/javascript"> //<![CDATA[ //(c) 2005 Jon Evans //hereby released under terms of the GNU General Public License, version 2 //On-load code //Create a map var friends=null; var xmlDoc=null; var map = null; function onLoad() { map = new GMap(document.getElementById("map")); map.addControl(new GLargeMapControl()); map.addControl(new GMapTypeControl()); var request = GXmlHttp.create(); request.open("GET", "mappable-xml/"+userName+".xml", true); request.onreadystatechange = function() { if (request.readyState == 4) { xmlDoc = request.responseXML; //Center the map on the root user //hack! hack! the root user is the first one in the list friends = xmlDoc.documentElement.getElementsByTagName("lj-user"); var root = friends[0] var latitudes = root.getElementsByTagName("latitude"); var rootLatitude = latitudes[0].firstChild.nodeValue; if (rootLatitude=="unknown") { rootLatitude=0; } var longitudes = root.getElementsByTagName("longitude"); var rootLongitude = longitudes[0].firstChild.nodeValue; if (rootLongitude=="unknown") { rootLongitude=0; } map.centerAndZoom(new GPoint(rootLongitude, rootLatitude), 15); //Get the as-of date var fings = xmlDoc.documentElement.getElementsByTagName("lj-friendings"); var listRoot = fings[0]; var asOfAttr = listRoot.getAttribute("as-of"); if (asOfAttr!="") { var htmlString="<font size=\"-1\">Friends data as of "; htmlString+=asOfAttr.substring(4,10); htmlString+=asOfAttr.substring(19,24); htmlString+=" (<a href=\"http://rezendi.zigamorph.net/\">regenerate</a>)</font>" var div=document.getElementById("asOfDate"); div.innerHTML=htmlString; } } } request.send(null); } //onClick accessors function doAll() { return generateMarkers(userBuckets.all) } function doFriends() { return generateMarkers(userBuckets.friends) } function doMutual() { return generateMarkers(userBuckets.mutual) } function doFriendedBy() { return generateMarkers(userBuckets.friendedBy) } function doStalking() { return generateMarkers(userBuckets.stalking) } function doStalkedBy() { return generateMarkers(userBuckets.stalkedBy) } //LJ User object, buckets, and populate function LJUser(name, isFriend, hasFriended, latitude, longitude, userLocation) { this.username=name; this.isFriend=isFriend; this.hasFriended=hasFriended; this.latitude=latitude; this.longitude=longitude; this.userLocation=userLocation; } function userBuckets() {} userBuckets.all = new Array(); userBuckets.friends = new Array(); userBuckets.mutual = new Array(); userBuckets.friendedBy = new Array(); userBuckets.stalking = new Array(); userBuckets.stalkedBy = new Array(); userBuckets.unknown = new Array(); userBuckets.unfound= new Array(); userBuckets.populated = false; function populateUserBuckets() { if (userBuckets.populated) return; //Kinda ugly. Switch to XSLT? //Start with 1 because 0 is the root user his/herself. for (var i = 1; i < friends.length; i++) { var usernames = friends[i].getElementsByTagName("username"); var username= usernames[0].firstChild.nodeValue; var isFriend = friends[i].getElementsByTagName("friended"); var isFriend = isFriend[0].firstChild.nodeValue; var hasFriendeds = friends[i].getElementsByTagName("friendedby"); var hasFriended = hasFriendeds[0].firstChild.nodeValue; var latitudes = friends[i].getElementsByTagName("latitude"); var latitude = latitudes[0].firstChild.nodeValue; var longitudes = friends[i].getElementsByTagName("longitude"); var longitude = longitudes[0].firstChild.nodeValue; var userLocations = friends[i].getElementsByTagName("location-name"); var userLocation = userLocations [0].firstChild.nodeValue; user = new LJUser(username, isFriend, hasFriended, latitude, longitude, userLocation); //Put in the appropriate bucket(s) if (userLocation=="blank" || userLocation=="unknown") { userBuckets.unknown.push(user); continue; } if (isNaN(latitude) || isNaN(longitude)) { userBuckets.unfound.push(user); continue; } //user's location is known; add to appropriate buckets userBuckets.all.push(user); if (isFriend=="True") { userBuckets.friends.push(user); if (hasFriended=="True") { userBuckets.friendedBy.push(user); userBuckets.mutual.push(user); } else { userBuckets.stalking.push(user); } } else { userBuckets.friendedBy.push(user); if (isFriend=="False") { userBuckets.stalkedBy.push(user); } } // if isFriend else } userBuckets.populated=true; } function MarkerData(latitude, longitude, locationName, html) { this.latitude=latitude; this.longitude=longitude; this.locationName=locationName; this.html=html; this.count=0; } MarkerData.prototype.latitude="unknown"; MarkerData.prototype.longitude="unknown"; MarkerData.prototype.locationName=""; MarkerData.prototype.html=""; MarkerData.prototype.count=0; function createMarker(amd) { var point = new GPoint(amd.longitude, amd.latitude); var marker = new GMarker(point); GEvent.addListener(marker, "click", function() { marker.openInfoWindowHtml("<b>"+amd.locationName+"</b><br>\n"+amd.html); }); return marker; } function myMarkers() {} function generateMarkers(userBucket) { //wipe existing data map.clearOverlays(); populateUserBuckets(); myMarkers.bucket=new Array(); for (var j=0; j<userBucket.length; j++) { var user=userBucket[j]; var existingMD = getExistingMarkerFor(user.latitude, user.longitude, user.userLocation) var usernameString=getLJHTMLFor(user.username); var thisMD; if (existingMD != undefined) { usernameString=usernameString+", "; if (existingMD.count++ % 5 == 0) { usernameString=usernameString+"<br>\n"; } existingMD.html = usernameString+existingMD.html; thisMD=existingMD; } else { thisMD= new MarkerData(user.latitude, user.longitude, user.userLocation, usernameString); } //Uniqueness hack for latitude-longitude var latLngString = new String(thisMD.latitude+":"+thisMD.longitude); myMarkers.bucket[latLngString]=thisMD; } //for loop //we've collected all the marker data; now create the markers for (var latLngKey in myMarkers.bucket) { var amd = myMarkers.bucket[latLngKey]; //without this if, we sometimes get a ghost marker - no idea why if (amd != undefined && amd.locationName!=undefined ) { marker = createMarker(amd); map.addOverlay(marker); } } } //O(n^2) - disastrous for long friendslists //various ways to optimize, but second rule of optimization applies function getExistingMarkerFor(userLat, userLong, userLocName) { for (var markerKey in myMarkers.bucket) { marker=myMarkers.bucket[markerKey]; lat1=parseFloat(marker.latitude); lat2=parseFloat(userLat); if (Math.abs(lat1-lat2)<1) { long1=parseFloat(marker.longitude); long2=parseFloat(userLong); if (Math.abs(long1-long2)<1) { //lat-longs are very close; how about the names? markerLocWord=marker.locationName.split(",")[0]; userLocWord=userLocName.split(",")[0]; if (markerLocWord.toUpperCase() == userLocWord.toUpperCase()) return marker; } } } // old, quick-n-dirty version // return myMarkers.bucket[userLat+":"+userLong]; } function doUnknown() { populateUserBuckets(); var outputString="<b>Undisclosed Locations:</b><BR>\n"; for (var k=0; k<userBuckets.unknown.length; k++) { var user=userBuckets.unknown[k]; if (k==0) { outputString=outputString+getLJHTMLFor(user.username); } else { outputString=outputString+", " if (k%5==0) { outputString=outputString+"<BR>\n"; } outputString=outputString+getLJHTMLFor(user.username); } } //for loop map.openInfoWindowHtml(map.getCenterLatLng(), outputString); } function doUnfound() { populateUserBuckets(); var outputString="<b>Google can't find:</b><BR>\n"; for (var k=0; k<userBuckets.unfound.length; k++) { var user=userBuckets.unfound[k]; if (k==0) { outputString=outputString+getLJHTMLFor(user.username); } else { outputString=outputString+", " if (k%2==0) { outputString=outputString+"<BR>\n"; } outputString=outputString+getLJHTMLFor(user.username); } outputString=outputString+"("+user.userLocation+")"; } //for loop map.openInfoWindowHtml(map.getCenterLatLng(), outputString); } //]]> </script></font></td></tr> <tr><td align='left' valign='top'><div id="asOfDate"><font size="-1">Friends data as of 24 Jun 2005 (<a href="http://rezendi.zigamorph.net/">regenerate</a>)</font></div></td><td align='right' valign='top'><font size="-2">(Missile launch functionality coming in version 2.0, contingent on DoD funding approval)</font></td> </tr></table></body> </html> </PLAINTEXT> </BODY> </HTML>