{"id":1325,"date":"2010-05-28T21:31:35","date_gmt":"2010-05-28T21:31:35","guid":{"rendered":"http:\/\/dalelane.co.uk\/blog\/?p=1325"},"modified":"2012-01-15T22:36:31","modified_gmt":"2012-01-15T22:36:31","slug":"my-google-latitude-history-as-a-heat-map","status":"publish","type":"post","link":"https:\/\/dalelane.co.uk\/blog\/?p=1325","title":{"rendered":"My Google Latitude History as a heat map"},"content":{"rendered":"<hr \/>\n<p><strong>Update (8 May 2011):<\/strong> I revisited this a year later to make a <a href=\"http:\/\/dalelane.co.uk\/blog\/?p=1703\">version that you can try online with your own Latitude data<\/a><br \/>\n<strong>Update (15 Jan 2012):<\/strong> A <a href=\"http:\/\/dalelane.co.uk\/blog\/?p=1325#fixed\">fixed version of the code<\/a> to handle the new Google Latitude file format.<\/p>\n<hr \/>\n<p><a target=\"_blank\" href=\"http:\/\/www.google.com\/latitude\/\">Google Latitude<\/a> is starting to get very interesting. The <a href=\"http:\/\/www.google.com\/latitude\/apps\/history\/dashboard\" target=\"_blank\">new dashboard<\/a> lets you see some graphs of how much time you spend at work, home, and out and about, and a list of your most visited places. <\/p>\n<p>You can also see a Google Map with your 500 latest updates added as pushpins. <\/p>\n<p>I <a href=\"http:\/\/twitter.com\/dalelane\/status\/14930337193\" target=\"_blank\">had a random idea<\/a> while looking at it this evening &#8211; why don&#8217;t they let you see <strong>all<\/strong> your updates on a map, in a heatmap that shows where you&#8217;ve been?<\/p>\n<p>Naturally, once I had the idea, I had to give it a quick try. <\/p>\n<p><a target=\"_blank\" href=\"http:\/\/maps.google.co.uk\/maps?f=q&#038;source=embed&#038;hl=en&#038;geocode=&#038;q=http:%2F%2Fdl.dropbox.com%2Fu%2F2738296%2Fgooglelatitude-heatmap-cumul.kml&#038;sll=53.067627,-4.042969&#038;sspn=21.392373,44.780273&#038;ie=UTF8&#038;ll=51.549751,-1.450195&#038;spn=4.782988,9.360352&#038;z=7\">This is the result<\/a>:<\/p>\n<p><iframe loading=\"lazy\" width=\"445\" height=\"350\" frameborder=\"0\" scrolling=\"no\" marginheight=\"0\" marginwidth=\"0\" src=\"http:\/\/maps.google.co.uk\/maps?f=q&amp;source=s_q&amp;hl=en&amp;geocode=&amp;q=http:%2F%2Fdl.dropbox.com%2Fu%2F2738296%2Fgooglelatitude-heatmap-cumul.kml&amp;sll=53.067627,-4.042969&amp;sspn=21.392373,44.780273&amp;ie=UTF8&amp;ll=51.549751,-1.450195&amp;spn=4.782988,9.360352&amp;z=7&amp;output=embed\"><\/iframe><br \/><small><a href=\"http:\/\/maps.google.co.uk\/maps?f=q&amp;source=embed&amp;hl=en&amp;geocode=&amp;q=http:%2F%2Fdl.dropbox.com%2Fu%2F2738296%2Fgooglelatitude-heatmap-cumul.kml&amp;sll=53.067627,-4.042969&amp;sspn=21.392373,44.780273&amp;ie=UTF8&amp;ll=51.549751,-1.450195&amp;spn=4.782988,9.360352&amp;z=7\" style=\"color:#0000FF;text-align:left\">View Larger Map<\/a><\/small> <\/p>\n<p>From the Google Latitude dashboard, you can export your history of location updates as a KML file. I downloaded my history, and wrote a short, hacky Python script to parse it, and generate a heat map to overlay on a Google map. <\/p>\n<p>In this post, I&#8217;ll show the sorts of results it can generate, and share my script, in case any other Latitude users fancy giving it a go. <\/p>\n<p><!--more-->I&#8217;ve been using Google Latitude for several months now, so I&#8217;ve got a reasonable chunk of data to play with. <\/p>\n<p>If I draw smaller points on a map, it&#8217;s easier to see the individual points:<\/p>\n<p><iframe loading=\"lazy\" width=\"450\" height=\"350\" frameborder=\"0\" scrolling=\"no\" marginheight=\"0\" marginwidth=\"0\" src=\"http:\/\/maps.google.co.uk\/maps?f=q&amp;source=s_q&amp;hl=en&amp;geocode=&amp;q=http:%2F%2Fdl.dropbox.com%2Fu%2F2738296%2Fgooglelatitude-heatmap-points.kml&amp;sll=51.230538,-1.097946&amp;sspn=0.173492,0.480652&amp;ie=UTF8&amp;ll=51.179343,-1.120605&amp;spn=0.602628,1.235962&amp;z=9&amp;output=embed\"><\/iframe><br \/><small><a href=\"http:\/\/maps.google.co.uk\/maps?f=q&amp;source=embed&amp;hl=en&amp;geocode=&amp;q=http:%2F%2Fdl.dropbox.com%2Fu%2F2738296%2Fgooglelatitude-heatmap-points.kml&amp;sll=51.230538,-1.097946&amp;sspn=0.173492,0.480652&amp;ie=UTF8&amp;ll=51.179343,-1.120605&amp;spn=0.602628,1.235962&amp;z=9\" style=\"color:#0000FF;text-align:left\">View Larger Map<\/a><\/small><\/p>\n<p><iframe loading=\"lazy\" width=\"445\" height=\"350\" frameborder=\"0\" scrolling=\"no\" marginheight=\"0\" marginwidth=\"0\" src=\"http:\/\/maps.google.co.uk\/maps?f=q&amp;source=s_q&amp;hl=en&amp;geocode=&amp;q=http:%2F%2Fdl.dropbox.com%2Fu%2F2738296%2Fgoogle-latitude-heatmap-eastleigh-large.kml&amp;sll=51.021314,-1.384964&amp;sspn=0.021785,0.060081&amp;ie=UTF8&amp;ll=50.981128,-1.357155&amp;spn=0.037826,0.076389&amp;z=13&amp;output=embed\"><\/iframe><br \/><small><a href=\"http:\/\/maps.google.co.uk\/maps?f=q&amp;source=embed&amp;hl=en&amp;geocode=&amp;q=http:%2F%2Fdl.dropbox.com%2Fu%2F2738296%2Fgoogle-latitude-heatmap-eastleigh-large.kml&amp;sll=51.021314,-1.384964&amp;sspn=0.021785,0.060081&amp;ie=UTF8&amp;ll=50.981128,-1.357155&amp;spn=0.037826,0.076389&amp;z=13\" style=\"color:#0000FF;text-align:left\">View Larger Map<\/a><\/small><\/p>\n<p>I&#8217;ve made the script configurable &#8211; the <a href=\"http:\/\/www.flickr.com\/photos\/dalelane\/sets\/72157624156114094\/\" target=\"_blank\">images in this flickr set<\/a> show the effects of tweaking some of these parameters. <\/p>\n<p>For example, playing with colours, the size of the dots added to the map, the opacity of the generated image, and so on. <\/p>\n<p>I also added a parameter to let you group locations that are near each other into single groups &#8211; it makes a difference to the type of heat map you get out of it. <\/p>\n<p><object width=\"440\" height=\"330\"><param name=\"flashvars\" value=\"offsite=true&#038;lang=en-us&#038;page_show_url=%2Fphotos%2Fdalelane%2Fsets%2F72157624156114094%2Fshow%2F&#038;page_show_back_url=%2Fphotos%2Fdalelane%2Fsets%2F72157624156114094%2F&#038;set_id=72157624156114094&#038;jump_to=\"><\/param><param name=\"movie\" value=\"http:\/\/www.flickr.com\/apps\/slideshow\/show.swf?v=71649\"><\/param><param name=\"allowFullScreen\" value=\"true\"><\/param><embed type=\"application\/x-shockwave-flash\" src=\"http:\/\/www.flickr.com\/apps\/slideshow\/show.swf?v=71649\" allowFullScreen=\"true\" flashvars=\"offsite=true&#038;lang=en-us&#038;page_show_url=%2Fphotos%2Fdalelane%2Fsets%2F72157624156114094%2Fshow%2F&#038;page_show_back_url=%2Fphotos%2Fdalelane%2Fsets%2F72157624156114094%2F&#038;set_id=72157624156114094&#038;jump_to=\" width=\"440\" height=\"330\"><\/embed><\/object><\/p>\n<p><a href=\"http:\/\/www.flickr.com\/photos\/dalelane\/4648564886\/\" title=\"my Google Latitude updates as a heatmap by dalelane, on Flickr\"><img loading=\"lazy\" decoding=\"async\" align=\"left\" hspace=\"10\" vspace=\"10\" src=\"http:\/\/farm5.static.flickr.com\/4066\/4648564886_e1bd13f9db_m.jpg\" width=\"240\" height=\"159\" alt=\"my Google Latitude updates as a heatmap\" \/><\/a>Another of the things I tried was making the density of marks on the heatmap proportional to the time spent at a location, rather than the number of updates made there (by comparing the timestamps of subsequent updates, and assuming that the time in between was spent at that location). <\/p>\n<p>I wasn&#8217;t sure whether the number of updates at a location is proportional to the time I spent there &#8211; so it&#8217;s another thing to play with. <\/p>\n<p>It&#8217;s interesting to see the clusters of marks around places that I regularly go to &#8211; such as points on the morning school run, or home and work. I&#8217;m not claiming that my script is 100% reliable or accurate, but on the whole the results look pretty believable \ud83d\ude42 <\/p>\n<p>Here is my script &#8211; it&#8217;s a quick-and-dirty hack, and far from optimal. But it&#8217;s a fun snippet to play with. <\/p>\n<pre style=\"border: thin solid silver; background-color: #eeeeee; padding: 0.7em; font-size: 1.1em; overflow: auto;\">################################################################################\r\n# Google Latitude Heat Map \r\n#\r\n#     Dale Lane (dale.lane@gmail.com)\r\n#           28 May 2010\r\n################################################################################ \r\n#  needs:\r\n#    heatmap - from http:\/\/jjguy.com\/heatmap\/\r\n#    KML file exported from Google Latitude\r\n################################################################################\r\n#  two parts to the script:\r\n#     1) parsing KML downloaded from http:\/\/www.google.com\/latitude\/apps\/history\/view\r\n#     2) adding lat\/lon points extracted from KML to a heatmap \r\n################################################################################\r\n#  warning and disclaimer: \r\n#    Provided as-is. \r\n#    This was a very quick bit of fun on a quiet evening - it's almost certainly\r\n#     got a ton of bugs and misunderstandings in it. Take it's results with a \r\n#     pinch of salt!\r\n################################################################################\r\n# imports\r\nimport xml.parsers.expat\r\nimport heatmap\r\n\r\n################################################################################\r\n#\r\n# VALUES TO TWEAK THE BEHAVIOUR OF THE SCRIPT\r\n#\r\n################################################################################\r\n\r\nLATLON_ACCURACY = 6        # how many decimal places should we round the lat\/lon\r\n                           #  values to?\r\n                           # reduce this to group similar locations together into\r\n                           #  a single larger group\r\n\r\nBOUNDS_LAT_MAX = 90        # range of latitude to include\r\n                           #  set to 90 to get everything\r\nBOUNDS_LAT_MIN = -90       # range of latitude to include\r\n                           #  set to -90 to get everything\r\nBOUNDS_LON_MAX = 180       # range of longitude to include\r\n                           #  set to 180 to get everything\r\nBOUNDS_LON_MIN = -180      # range of longitude to include\r\n                           #  set to -180 to get everything\r\n\r\n# for example, in some of my maps, I set the bounds to draw a map of \r\n#   locations in and around Eastleigh\r\n# BOUNDS_LAT_MAX = 51.033190218589546\r\n# BOUNDS_LAT_MIN = 50.95929172950454\r\n# BOUNDS_LON_MAX = -1.3039398193359375\r\n# BOUNDS_LON_MIN = -1.4261627197265625\r\n\r\nADJUST_DENSITY_FOR_TIME = False  # False - we add every location to the map once\r\n                                 #          for every time it was recorded by \r\n                                 #          Google Latitude\r\n                                 # True - we add each location to the map a number\r\n                                 #          of times that is proportional to the \r\n                                 #          total amount of time spent there\r\n\r\nDENSITY_ADJUST_HOURS = 12    # if ADJUST_DENSITY_FOR_TIME is True, then we will \r\n                             #  add each location to the heatmap once for this \r\n                             #  number of hours spent at the location\r\n                             # e.g. if this number is 12, we will add the \r\n                             #  location to the heatmap once for every 12 hours\r\n                             #  spent there in total\r\n\r\nHEATMAP_DOT_SIZE = 2    # how large should points plotted on the heatmap be?\r\n                        #  value is in pixels\r\n                        #  2 is the smallest possible value\r\n\r\nHEATMAP_DOT_OPACITY = 210    # do you want to be able to see the map through the\r\n                             #  heatmap overlay?\r\n                             # change opacity between 0 and 255 to adjust\r\n\r\nGOOGLE_LATITUDE_FILE = \"kml.kml\"   # file name of KML from Google Latitude\r\n\r\n\r\n################################################################################\r\n# globals\r\n################################################################################\r\n# points we will plot on the heat map\r\npts = []\r\n\r\n\r\nclass MyKMLParser:\r\n  \r\n    # which XML element are we on?  \r\n    currentelement = None\r\n\r\n    # the timestamp (in milliseconds since epoch) of the last placemark\r\n    previoustime = None\r\n    # the timestamp (in milliseconds since epoch) of the current placemark\r\n    latesttime = None\r\n    \r\n    # the latitude of the current placemark\r\n    latestlat = None\r\n    # the longitude of the current placemark\r\n    latestlon = None  \r\n\r\n    # two-dimensional array of time spent at locations\r\n    # timespent[lat][lon] = totalTimeInMilliseconds\r\n    timespent = {}\r\n    \r\n    # XML parser function - StartElementHandler\r\n    def start_element(self, name, attrs):    \r\n        latestelement = str(name)\r\n        if latestelement == \"Data\":\r\n            # there are multiple data elements - it's the 'name' attribute which \r\n            #  identifies these element\r\n            self.currentelement = attrs[\"name\"]\r\n        elif latestelement != \"value\":\r\n            # value elements are always within a uniquely named element, so \r\n            #  we ignore the 'value' tag - it's the parent that tells us what\r\n            #  this\r\n            self.currentelement = latestelement\r\n\r\n    # XML parser function - EndElementHandler\r\n    def end_element(self, name):  \r\n        global ADJUST_DENSITY_FOR_TIME, pts\r\n        if name == \"Placemark\":\r\n            if self.latestlat != None and self.latestlon != None:\r\n                if ADJUST_DENSITY_FOR_TIME == True:\r\n                    #\r\n                    # calculate the time difference between this placemark \r\n                    #  and the previous one\r\n                    #\r\n                    \r\n                    # default to 10 minutes - for no particularly good reason\r\n                    valueMilliseconds = 10 * 60 * 1000\r\n                    if self.previoustime != None:\r\n                        valueMilliseconds = self.previoustime - self.latesttime\r\n                    self.previoustime = self.latesttime\r\n        \r\n                    #\r\n                    # store the time spent in this location\r\n                    #\r\n                    if self.latestlat not in self.timespent:\r\n                        self.timespent[self.latestlat] = {}\r\n                    if self.latestlon not in self.timespent[self.latestlat]:\r\n                        self.timespent[self.latestlat][self.latestlon] = 0\r\n    \r\n                    self.timespent[self.latestlat][self.latestlon] += valueMilliseconds\r\n                else:\r\n                    #\r\n                    # plot this point - we don't care how about how long was \r\n                    #  spent here\r\n                    #\r\n                    pts.append((self.latestlon, self.latestlat))\r\n        \r\n        if name != \"Data\":\r\n            self.currentelement = \"\"\r\n\r\n    # XML parser function - CharacterDataHandler\r\n    def char_data(self, data):\r\n        global LATLON_ACCURACY, BOUNDS_LAT_MAX, BOUNDS_LAT_MIN, BOUNDS_LON_MAX, BOUNDS_LON_MIN\r\n\r\n        if self.currentelement == \"coordinates\":\r\n            #\r\n            # location data - looks like <coordinates>-1.355025,50.973697,0<\/coordinates>\r\n            #  so we split it on commas, and take the first and second value\r\n            #\r\n\r\n            # initialise values\r\n            self.latestlat = None\r\n            self.latestlon = None\r\n\r\n            tmplatestcoordinates = str(data)\r\n            coordbits = tmplatestcoordinates.split(',')\r\n            try:\r\n                if len(coordbits) == 3:\r\n                    lat = float(coordbits[1])\r\n                    lon = float(coordbits[0])\r\n\r\n                    if lon < = BOUNDS_LON_MAX and lon >= BOUNDS_LON_MIN and lat < = BOUNDS_LAT_MAX and lat >= BOUNDS_LAT_MIN:\r\n                        self.latestlat = round(lat, LATLON_ACCURACY)\r\n                        self.latestlon = round(lon, LATLON_ACCURACY)\r\n            except:\r\n                noop = 0\r\n\r\n        elif self.currentelement == \"timestamp\":\r\n            #\r\n            # timestamp - looks like  <data name=\"timestamp\"><value>1274989863180<\/value><\/data>\r\n            #  the value is in milliseconds since the epoch\r\n            #\r\n\r\n            try:\r\n                newlatesttime = int(data)\r\n                # sanity check - is it in the range I expect?\r\n                #  these are hard-coded values for the times I know that I've \r\n                #   been using latitude\r\n                if newlatesttime > 1254355200000 and newlatesttime < 1275043497000:\r\n                    self.latesttime = newlatesttime\r\n            except Exception, exc:\r\n                noop = 0\r\n\r\n\r\n################################################################################\r\n#\r\n# parse the KML \r\n#\r\n################################################################################\r\nparser = MyKMLParser()\r\np = xml.parsers.expat.ParserCreate()\r\np.StartElementHandler  = parser.start_element\r\np.EndElementHandler    = parser.end_element\r\np.CharacterDataHandler = parser.char_data\r\n\r\np.ParseFile(open(GOOGLE_LATITUDE_FILE))\r\n\r\n################################################################################\r\n#\r\n# draw the extracted points on a heat map \r\n#\r\n################################################################################\r\n\r\n# if we are adjusting the density based on how long was spent at locations, \r\n#   we need to do this now\r\n# if not, we added points to the heat-map store as we parsed the KML, so there\r\n#   is nothing else to do \r\n\r\nif ADJUST_DENSITY_FOR_TIME == True:\r\n    # collect the points to draw\r\n    for lat in parser.timespent:\r\n        for lon in parser.timespent[lat]:\r\n            # how long did we spend at this location?\r\n            #  so that the heat map reflects the time spent at locations, we \r\n            #   add a point multiple times \r\n            timeSpent = parser.timespent[lat][lon] \/ (1000 * 60 * 60 * DENSITY_ADJUST_HOURS)\r\n            if timeSpent > 0:\r\n                for n in range(timeSpent):\r\n                    pts.append((lon, lat))\r\n            else:\r\n                pts.append((lon, lat))\r\n\r\nprint str(len(pts)) + \" points to plot\"\r\n\r\n# draw the heatmap using the awesome http:\/\/jjguy.com\/heatmap\/\r\nhm = heatmap.Heatmap()\r\nhm.heatmap(pts, \r\n           \"google-latitude-heatmap.png\", \r\n           scheme='pbj', \r\n           opacity=HEATMAP_DOT_OPACITY, \r\n           dotsize=HEATMAP_DOT_SIZE)\r\nhm.saveKML(\"google-latitude-heatmap.kml\")<\/pre>\n<p>Finally, I have to say a big thanks to the <a href=\"http:\/\/jjguy.com\/heatmap\/\" target=\"_blank\">author of the library I used to generate the heatmaps<\/a> &#8211; which is the only way I was able to hack this together in an hour or so. <\/p>\n<p>.<br \/>\n<a name=\"fixed\"><\/a><br \/>\n<strong>Update (15-Jan-2012):<\/strong> Since writing this post, Google changed the format of the files exported by Latitude. I didn&#8217;t have a chance to rewrite my code to work with the new file format, but <a href=\"http:\/\/dmd.3e.org\/\">Daniel Drucker<\/a> fixed it to make <a href=\"http:\/\/imgur.com\/a\/gfzIH\">his heatmaps<\/a> and was kind enough to share his version of the code with me:<\/p>\n<pre style=\"border: thin solid silver; background-color: #eeeeee; padding: 0.7em; font-size: 1.1em; overflow: auto;\">#!\/usr\/bin\/python\r\n\r\nimport time\r\n\r\nLATLON_ACCURACY = 8        # how many decimal places should we round the lat\/lon\r\n                           #  values to?\r\n                           # reduce this to group similar locations together into\r\n                           #  a single larger group\r\n\r\nBOUNDS_LAT_MAX = 90        # range of latitude to include\r\n                           #  set to 90 to get everything\r\nBOUNDS_LAT_MIN = -90       # range of latitude to include\r\n                           #  set to -90 to get everything\r\nBOUNDS_LON_MAX = 180       # range of longitude to include\r\n                           #  set to 180 to get everything\r\nBOUNDS_LON_MIN = -180      # range of longitude to include\r\n                           #  set to -180 to get everything\r\n\r\n\r\nADJUST_DENSITY_FOR_TIME = True   # False - we add every location to the map once\r\n                                 #          for every time it was recorded by\r\n                                 #          Google Latitude\r\n                                 # True - we add each location to the map a number\r\n                                 #          of times that is proportional to the\r\n                                 #          total amount of time spent there\r\n\r\nDENSITY_ADJUST_HOURS = 24    # if ADJUST_DENSITY_FOR_TIME is True, then we will\r\n                             #  add each location to the heatmap once for this\r\n                             #  number of hours spent at the location\r\n                             # e.g. if this number is 12, we will add the\r\n                             #  location to the heatmap once for every 12 hours\r\n                             #  spent there in total\r\n\r\nGOOGLE_LATITUDE_FILE = \"kml\"   # file name of KML from Google Latitude\r\n\r\n\r\n################################################################################\r\n# globals\r\n################################################################################\r\n# points we will plot on the heat map\r\npts = []\r\n\r\nprevioustime = None\r\nlatesttime = None\r\nlatestlat = None\r\nlatestlon = None\r\n\r\n# two-dimensional array of time spent at locations\r\n# timespent[lat][lon] = totalTimeInMilliseconds\r\ntimespent = {}\r\ni=0\r\ndatalines=[]\r\nfor line in open(GOOGLE_LATITUDE_FILE,'r'):\r\n    if 'gx:coord' in line:\r\n        (lon,lat,x) = line.replace('&lt;gx:coord&gt;','').split(' ')\r\n        lat = round(float(lat), LATLON_ACCURACY)\r\n        lon = round(float(lon), LATLON_ACCURACY)\r\n        i += 1\r\n    if 'when' in line:\r\n        t = time.mktime(time.strptime(line[6:25],'%Y-%m-%dT%H:%M:%S'))\r\n        i += 1\r\n    if i==2:\r\n        datalines.append(dict(lat=lat,lon=lon,t=t))\r\n        i=0\r\n\r\nfor thisone in datalines:\r\n    latestlat = thisone['lat']\r\n    latestlon = thisone['lon']\r\n    latesttime = thisone['t']\r\n\r\n    if ADJUST_DENSITY_FOR_TIME == True:\r\n        #\r\n        # calculate the time difference between this placemark\r\n        #  and the previous one\r\n        #\r\n\r\n        # default to 10 minutes - for no particularly good reason\r\n        valueMilliseconds = 10 * 60 * 1000\r\n        if previoustime != None:\r\n            valueMilliseconds = previoustime - latesttime\r\n        previoustime = latesttime\r\n\r\n        #\r\n        # store the time spent in this location\r\n        #\r\n        if latestlat not in timespent:\r\n            timespent[latestlat] = {}\r\n        if latestlon not in timespent[latestlat]:\r\n            timespent[latestlat][latestlon] = 0\r\n\r\n        timespent[latestlat][latestlon] += valueMilliseconds\r\n    else:\r\n        # plot this point - we don't care how about how long was\r\n        #  spent here\r\n        pts.append((latestlon, latestlat))\r\n\r\n\r\n################################################################################\r\n#\r\n# draw the extracted points on a heat map\r\n#\r\n################################################################################\r\n\r\n# if we are adjusting the density based on how long was spent at locations,\r\n#   we need to do this now\r\n# if not, we added points to the heat-map store as we parsed the KML, so there\r\n#   is nothing else to do\r\n\r\nif ADJUST_DENSITY_FOR_TIME == True:\r\n    # collect the points to draw\r\n    for lat in timespent:\r\n        for lon in timespent[lat]:\r\n            # how long did we spend at this location?\r\n            #  so that the heat map reflects the time spent at locations, we\r\n            #   add a point multiple times\r\n            timeSpent = timespent[lat][lon] \/ (1000 * 60 * 60 * DENSITY_ADJUST_HOURS)\r\n            if timeSpent &gt; 0:\r\n                for n in range(int(timeSpent)):\r\n                    pts.append((lon, lat))\r\n            else:\r\n                pts.append((lon, lat))\r\n\r\n# draw the heatmap\r\nprint \"var coordinates = [\"\r\nfor point in pts[:-1]:\r\n    print \"[ \" + str(point[1]) + \", \" + str(point[0]) + \" ],\"\r\nprint \"[ \" + str(pts[-1][1]) + \", \" + str(pts[-1][0]) + \" ]\"\r\nprint \"];\" <\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Update (8 May 2011): I revisited this a year later to make a version that you can try online with your own Latitude data Update (15 Jan 2012): A fixed version of the code to handle the new Google Latitude file format. Google Latitude is starting to get very interesting. The new dashboard lets you [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[384,158,387,137,212],"class_list":["post-1325","post","type-post","status-publish","format-standard","hentry","category-code","tag-geolocation","tag-google","tag-latitude","tag-location","tag-python"],"_links":{"self":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts\/1325","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1325"}],"version-history":[{"count":0,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts\/1325\/revisions"}],"wp:attachment":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1325"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1325"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1325"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}