20

On May 21, 2008, an xkcd comic proposed a method for creating a random, local meeting place that can't be determined until the day of the meeting.

Geohashing comic

This method is usually used for xkcd fan events, but can also be a fun way to create a random adventure (kinda like Geocaching without the cache.) More info about the specifics can be found on the Geohashing Wiki.

Since the random part of the hash is created from the Dow Jones Industrial Average, I thought it would be a fun algorithm to implement in Mathematica. I also wanted to create an excuse to learn about the Google Maps API and processing JSON data in general.

Given only a user's "home" coordinates, how would one compute the Geohash location in Mathematica?

J. M.'s missing motivation
  • 124,525
  • 11
  • 401
  • 574
Corey Kelly
  • 1,738
  • 9
  • 23
  • 3
    Better hope that the DJIA doesn't crash. Otherwise you could end up in the sea. – Oleksandr R. Jun 09 '13 at 19:42
  • In fact, some of the locations do end up being over water. An interesting modification would be to somehow prevent this using land boundary data... – Corey Kelly Jun 09 '13 at 21:38

1 Answers1

17

I separated this project into two parts. The first is to compute the coordinates of the Geohash location.

(*Grab the user's geographical location. The location is based on IP
address, so it may not be completely accurate. It's usually good
enough to get your graticute. You can replace home with with known
coordinates in the form {hx, hy} if you like.*)
home = FindGeoLocation[];

(*An alternative, using Wolfram Alpha
home=N[FromDMS/@WolframAlpha["Where am I?",
{{"HostInformationPod",1},"ComputableData"}][[3,2,1]]];
*)

(*The current date string, accounting for the 30W Time Zone Rule
 http://wiki.xkcd.com/geohashing/30W_Time_Zone_Rule *)
date = DateString[If[home[[2]] > -30, DatePlus[-1], DateString[]], 
 {"Year", "/", "Month", "/", "Day"}
];

(*Most recent Dow Jones Average*)
dj = URLFetch["http://carabiner.peeron.com/xkcd/map/data/" <> date];

(*Current date. Combine with Dow Jones average.*)
str = StringJoin[{DateString[{"Year", "-", "Month", "-", "Day", "-"}],
     dj}];

(*Perform the string hash. Note that in MMA versions before 8, this
will return inconsistent results. See
http://mathematica.stackexchange.com/questions/13529/why-does-hash-
return-different-values-in-version-7 for a workaround*)
hsh = RealDigits[Hash[str, "MD5"], 16][[1]];

(*Mathematica chops off leading zeroes, this puts them back on*)
ConstantArray[0, 32 - Length[hsh]]~Join~hsh;

(*Split to two coordinates*)
{xhex, yhex} = Partition[hsh, 16];

(*Convert to decimal*)
{xhex, yhex} = Prepend[#, 0] & /@ {xhex, yhex};
{x, y} = (FromDigits[{#, 1}, 16] & /@ {xhex, yhex});

(*Compute coordinates*)
coords = Sign[#] (Abs[#] + {x, y}) &[IntegerPart /@ home] // N;

We now have the coordinates for the Geohash. We can use Google Maps API to show some relevant map data and travel info:

(*Create strings for Google API from coordinate pairs*)
fmt[s_] := StringReplace[ToString[s], # -> "" & /@ {"{", "}", " "}]

(*Use Google Directions API to find path to Geohash*)
path = Import[
   "http://maps.googleapis.com/maps/api/directions/json?origin=" <> 
    fmt[home] <> "&destination=" <> fmt[coords] <> "&sensor=false", 
   "JSON"];
pathString = 
  First["points" /. 
    Cases[path, HoldPattern["overview_polyline" -> value_] :> value, 
     Infinity]];

(*Use Google Static Maps API to show start and end points along with \
path*)
mapPath = 
  "http://maps.googleapis.com/maps/api/staticmap?size=600x600&maptype=\
roadmap&markers=color:red%7C" <> fmt[home] <> 
   "&markers=color:blue%7C" <> fmt[coords] <> 
   "&path=weight:4%7Ccolor:red%7Cenc:" <> pathString <> 
   "&sensor=false";

(*A close-up map of the Geohash location*)
mapHash = 
  "http://maps.googleapis.com/maps/api/staticmap?center=" <> 
   fmt[coords] <> 
   "&zoom=14&size=600x600&maptype=roadmap&markers=color:red%7C" <> 
   fmt[coords] <> "&sensor=false";

(*Show maps*)
GraphicsGrid[{{Import[mapPath], Import[mapHash]}}, ImageSize -> 1000]

(*Extract and print trip time/distance info*)
Print[Style[
   "Geohash coordinates: " <> ToString[coords] <> 
    ", Trip Distance: " <> 
    First["text" /. 
      Cases[path, HoldPattern["distance" -> value_] :> value, 
       Infinity]] <> ", Driving Time: " <> 
    First["text" /. 
      Cases[path, HoldPattern["duration" -> value_] :> value, 
       Infinity]], Large]];

Which gives us something like this:

Geohash Output

I'd love to see any improvements or fun extensions that anybody can come up with!

EDIT:

After noticing that FinancialData[] only returns the DJI without decimals, I checked the Geohashing Wiki to see how other users grab this value and found several possibilities. Using

dj = URLFetch[
   "http://carabiner.peeron.com/xkcd/map/data/" <> 
    DateString[{"Year", "/", "Month", "/", "Day"}]];

Returns results that are consistent with the "Official" calculator.

EDIT 2:

While looking through other implementation details, I discovered the 30W Time Zone Rule. We can account for this with

DateString[
 If[home[[2]] > -30, 
  DatePlus[-1],
  DateString[]
 ], 
 {"Year", "/", "Month", "/", "Day"}
]

EDIT 3:

Alternative method for user's "home" coordinates using Wolfram Alpha:

home=N[FromDMS/@
WolframAlpha["Where am I?",{{"HostInformationPod",1},"ComputableData"}]
[[3,2,1]]
];
Corey Kelly
  • 1,738
  • 9
  • 23
  • Comparing to http://carabiner.peeron.com/xkcd/map/map.html I've noticed that my algorithm is a bit off because MMA returns financial data without decimals. Looking into this now! – Corey Kelly Jun 09 '13 at 18:53
  • Wow, you can still use FinancialData["^DJI", "Open"]? It hasn't been working for a while for me... – J. M.'s missing motivation Jun 09 '13 at 18:57
  • Seems to work here! 9.0.0.0 on Windows 7 (32-bit). Although I'm looking for an alternate Finance API, since MMA and Alpha both omit the decimal values of the DJI. – Corey Kelly Jun 09 '13 at 19:09
  • FinancialData["^DJI", All], however, returns Missing[NotAvailable]. – Corey Kelly Jun 09 '13 at 19:13
  • Anyway: ghash[date_, dji_] := With[{cs = IntegerString[Hash[DateString[date, {"Year", "-", "Month", "-", "Day"}] <> "-" <> dji, "MD5"], 16]}, IntegerPart[Take[FindGeoLocation[], 2]] + (N[FromDigits[StringReverse[StringTake[cs, 16 #]], 1/16]/16] & /@ {1, -1})] – J. M.'s missing motivation Jun 09 '13 at 19:16
  • I think you need IntegerPart[] instead of Round[] – Corey Kelly Jun 09 '13 at 19:28
  • 1
    Using that source at Peeron: ghash[date_, loc_: $GeoLocation] := Module[{d1 = DateString[date, {"Year", "/", "Month", "/", "Day"}], cs, dji, l2}, dji = Import["http://carabiner.peeron.com/xkcd/map/data/" <> d1]; cs = IntegerString[Hash[StringReplace[d1, "/" -> "-"] <> "-" <> dji, "MD5"], 16]; l2 = Take[loc, 2]; IntegerPart[l2] + Sign[l2] N[Table[FromDigits[StringReverse[StringTake[cs, 16 k]], 1/16], {k, {1, -1}}]/16]] – J. M.'s missing motivation Jun 09 '13 at 19:45
  • @CoreyKelly You can use &zoom=automatic (instead of an arbitrary &zoom=14) in your Static Map API request to facilitate visualization of path. – Rod Jun 09 '13 at 20:15
  • @CoreyKelly You can also use &markers=color:red%7Clabel:O%7C and &markers=color:red%7Clabel:D%7C to identify your O=origin and your D=destination. – Rod Jun 09 '13 at 20:20
  • @Rod: I used automatic zoom for the first image to show the start/end points along with the path. I chose 14 for the zoom of the second image because I thought it was a good "close-up" shot of the end location. – Corey Kelly Jun 09 '13 at 21:26
  • DJI parser in mathematica, with decimals: DJI = StringReplace[ Flatten[StringCases[ StringSplit[ Import["https://www.google.com/finance?q=INDEXDJX:.DJI"], "\n"], "Open" ~~ __]][[1]], {"Open" | "," | " " -> ""}] – shrx Jun 12 '13 at 18:03
  • If the location is over a body of water, path output is {"routes" -> {}, "status" -> "ZERO_RESULTS"} and the code for displaying the maps fails. – shrx Jun 12 '13 at 18:16
  • Thanks, shrx! How would you modify the DJI parser to return the opening index from the previous day? Note that this is required for 30W compliance. I had also just noticed the issue with locations over water. I could use a large If[] block to avoid the error, but I'm currently trying to coerce the Maps API into showing the location despite it not having an address. – Corey Kelly Jun 13 '13 at 10:41