Tutorial: Creating geographic games using Google Maps API



Tutorial: Creating geographic games using Google Maps API

This game was inspired by the Lufthansa to Europe Virtual Pilot game. For a Flash implementation of a virtual pilot game, I will be posting examples and a tutorial soon to my Flash examples page. The Flash example includes a flying object. It also includes testing against regions as well as points. Here, I describe another approach, one using JavaScript and Google Maps and XML. I did not attempt in the JavaScript game to produce a flying object. This would be possible but would have required provision for browser differences. (See the cannonball games in the HTML/JavaScript examples). I liked the idea of using Google Maps and also XML to make the game data-driven. I also liked using my own icons for markers, which is supported by the Google Map API. Visit .

Working in phases is an important part of application development. Please go to the Google Map API tutorial and try your hand at the examples, including making a map of your choice appear, with a marker of your design. Here I describe 4 programs:

1. ask one fixed question

2. use an XML file to ask some number of questions. The average miles off is displayed at the end. NOTE: higher numbers are worse than lower numbers!

3. use an XML file to define a set of questions, with a time limit for each question. An unanswered question receives a score of 100.

4. use an XML file to define a set of questions. Questions are selected at random from the list. There is a button to be pressed when the player wants to stop. The encoding for the locations can be in one of two formats.

As always, these examples are meant for instruction in general and specific concepts in programming. Please correct, enhance, and improve and send comments.

Basic requirements: Maps and Geographic features, event handling and calculations

The geography game requires

• generation or acquisition of a map,

• a way of encoding places, including the location of the place and the text used in posing the question

• setting up the event handler for the player clicking on the map

• calculating the distance from the right answer location to the location indicated by the player

The Google Maps API provided the map along with the construct for event handling for clicking on the map. The values returned are latitude and longitude.

I used XML to encode the question information and used the standard way of reading in an XML file to get this information into my JavaScript program, mainly several parallel arrays.

The latitude and longitude are measurements of a position on the globe. You can think of them as angles. The zero point for the latitude is the equator. The zero point for the longitude is what is termed the Greenwich meridian—this is Greenwich, England. You will need to know the latitude and longitude to create your own application. One way to do this is to use Google Maps. Go to and then click on Map. Put the location you want in the location field. Sample screen shot:

[pic]

Now click on the Link in the upper right corner. A pop-up window appears:

[pic]

The latitude and longitude are in both the links. Here is the Paste link in email or IM:



I have made the ll= parameters bold: 41.49945 and -73.968887.

See the explanation for the 4th game for a discussion on alternative formats for latitude and longitude.

The distance calculation is performed using the spherical law of cosines (source: ). [This article makes the point that the calculation requires the high precision calculation now available using JavaScript. Note also that this assumes the globe to be a sphere. It actually is NOT a perfect sphere, but the claim is the calculation is good enough for distances as small as one meter!] The latitude and longitude values need to be converted to radians. Radians are an intrinsic system of measurement (the system of degrees rests on an arbitrary choice of 360 for the total degrees in a circle.) A factor is used to convert to miles. The factor for kilometers is in the code as a comment.

Implementation

The application consists of an html / javascript file, an xml file for programs 2 and 3, image files for the marker icons AND access to Google Maps. Note: this particular application does not require obtaining a Google API Key.

The element of a html/javascript file using Google Maps starts off with the following meta and script tags:

The main Google Maps construct is a map! This is produced using a constructor function and placed into a div element on the HTML page. The data required includes the center of the map, indicated by a latitude longitude object, a zoom factor, and a type. The data is held in an associative array, myOptions in the code. The types allowed are: ROADMAP, SATELLITE, HYBRID and TERRAIN. Note that several lines are used for clarity only.

var map;

var latlng = new google.maps.LatLng(41.2, 286.27);

var myOptions = {

zoom: 9,

center: latlng,

mapTypeId: google.maps.MapTypeId.TERRAIN

};

map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

The last statement (map = new …) generates the map with the indicated options and puts it into the element with the id= "map_canvas".

Another critical construct for this application is a marker placed on a map. The parameters include the position (this is a latitude longitude object) and a map reference. Optionally, parameters for markers can include a title and an icon, the latter a reference to an image file. Notice that in these examples, the associative arrays holding the information are constructed 'on the spot', within the call of the method. Notice also that map refers to two different things in the clause map: map.

The first map is the name of the property in the array, similar to position, title and icon. The second map is the variable holding the constructed map.

var marker = new google.maps.Marker({position: location, map: map });

var mk = new google.maps.Marker(

{position: latlng,

map: map,

title: "Mt. Kisco",

icon: "chief.png" });

Google Maps provides a way to identify the event of clicking on the map and specifying a handler for the event. The syntax is similar, but not quite the same, as Flash ActionScript. The code is:

google.maps.event.addListener(map, 'click', function(event) { checkit(event.latLng); });

The 3rd parameter defines the function to be called when the 'click' event for map occurs. The argument to the function is something called event. The function consists of one line: a call to my checkit function using as argument event.latLng. That is, the event object has a property named latLng.

With this background, here are explanations of the code for the 3 programs.

Program 1: one question

Here is the opening screen shot for the one question program:

[pic]

Notice that the Google Map zooming and map type interface devices are displayed. I removed those from the next two versions.

Here is the screen after the player makes her guess of the location of Mt. Kisco.

[pic]

The upside-down tear shaped icon is the default for markers. The actual location of Mt. Kisco is indicated by my own programmer-supplied icon. This is a photo of what is called the Chief Kisco statue in the center of the Village of Mt. Kisco.

This program uses the Google Maps features described earlier. The application consists of this .html file plus the chief.png file. There are two functions: initialize and checkit. The initialize function is called by the onload attribute in the tag. The checkit function is called through the action of the addListener method. There are 2 global variables: map and latlng. These are made global so they can be used by both the initialize and checkit functions.

The code is

| | |

| | |

| |required by Google Maps API |

| | |

| |open script element |

|var map; |holds the map |

|var latlng = new google.maps.LatLng(41.2, 286.27); |A variable of the LatLng datatype |

| function initialize() { |start of initialize function |

| var myOptions = { |holds options for the map |

| zoom: 9, | |

| center: latlng, | |

| mapTypeId: google.maps.MapTypeId.TERRAIN | |

| }; | |

| map = new google.maps.Map(document.getElementById("map_canvas"), |create the map and place in the html document |

|myOptions); | |

| google.maps.event.addListener(map, 'click', function(event) { |set up for player clicks |

| checkit(event.latLng); |… handling done by checkit function |

| }); | |

| } |close initialize function |

| function checkit(location) { |start of checkit function |

| var marker = new google.maps.Marker({position: location, map: map }); |create the marker when the player clicked |

| var mk = new google.maps.Marker( |create the marker at the correct position |

| {position: latlng, | |

| map: map, | |

| title: "Mt. Kisco", icon: "chief.png" }); | |

| } |close checkit |

| | |

| | |

| |set up call to initialize |

|Click on Mt. Kisco? |Display prompt to player |

| |sets up place to put the map |

| | |

| | |

To make this application your own, determine using Google Maps the latitude and longitude of a specific point. Create an image file. Use your own values for title and for the name of the image file. Make sure the image file and your html file are in the same folder. Note: you also can replace the default marker image with one of your own.

Program 2: XML file

The next program uses an xml file to present a sequence of questions to the player. The file consists of one element and one or more elements. The element provides the information for the map object that serves as the base of the game. The elements provide the information for the questions. Keep in mind that the design of the xml file, what I made attributes and what I made elements and child elements, is all my own idea. Other designs would have worked equally well with the appropriate coding!

The xml file used for this example is

Westchester

chief.png

Mount Kisco

crotondam.png

Croton Dam

prison.png

Sing Sing

Notice that the lng values are negative, indicating westerly or counter-clockwise from the GMT meridian. The setting -73.867664 is the same as 360-73.867664, which is 286.1323.

The javascript in the html file reads in the xml file, an xml document is created, and arrays constructed using the xml data in the standard way. Note that this does involve some browser specific coding in the form of the catch and try clauses. The document is read in using async mode of operation. This means that the program waits for the file to be read. There is no error detection for a missing or badly formed xml file. Check out the other examples on my pages for how to do this. It would be appropriate to do this if you built a system with many different xml files. A variable turns is used to iterate over the questions.

In addition, the program performs a calculation of the distance from the player's guess (click on the map) to the correct position. This is done using the cosine formula mentioned above. When the list is completed—all places asked—the program calculates the average distance. This is a crude way of scoring that could be improved. It generally is better to make high scores be better than low scores. Moreover, it would make sense to make very small discrepancies not count.

The results after each question and then the average are displayed using elements in a element.

Here is a screen shot after completion of the set of questions

[pic]

The application consists of 3 functions: initialize, checkit and dist. As before, the initialize is invoked through the onload property of the element and the checkit through the action of the addListener method. The checkit function calls the dist function. The initialize function reads in the xml file and sets up the arrays and asks the first question. It also invokes the addListener method to respond to the player clicking on the map.

The additional global variables used for this program are:

|var markertitle; |Holds text used for question and for marker title |

|var turn; |Holds current turn in sequence of questions |

|var picnames; |Holds array of icon file names from xml file |

|var titles; |Holds array of titles from xml file |

|var count; |Holds total number of questions |

|var tots = 0; |Holds score |

|var listener; |Holds the event handling for clicking on the map |

I decided to remove the Google Map interface features letting the player/user zoom in or change the type of map. This is done by adding disableDefaultUI: true to the information in the myOptions array.

Note that the values extracted from the xml file are text and all that represent numbers need to be converted using the Number function to be used.

| | |

| | |

| | |

| | |

| |start script element |

|var map; |hold the map |

|var latlng; | hold a latlng object |

|var markertitle; |text for question and for title for marker |

|var turn; |keeps track of the turns |

|var picnames; |hold names of image files from the xml |

|var titles; |hold the titles/text |

|var count; |number of places |

|var tots = 0; |keeps running total of miles off from correct position |

|var listener; |listener object |

|function initialize() { |start of initialize function invoked onload |

| document.f.question.value = ""; |clear question |

| document.f.distance.value = ""; |clear distance |

| document.f.average.value = ""; |clear average |

| try //Internet Explorer |Used to determine what browser to get the xml document |

| { | |

| xmlDoc=new ActiveXObject("Microsoft.XMLDOM"); |Microsoft language for xml. create xml document |

| } | |

|catch(e) | |

| { | |

| try //Firefox, Mozilla, Opera, etc. |now try the language for other browsers |

| { | |

|xmlDoc=document.implementation.createDocument("","",null); |… create xml document |

| } | |

| catch(e) | |

| { | |

| alert(e.message); |Two methods failed, so can't create xml document |

| return; | |

| } | |

| } | |

|xmlDoc.async=false; |set up to wait for full loading of xml document |

|xmlDoc.load("game1.xml"); |Load the xml document named |

|turn=0; |initialize turn to 0 |

|picnames = xmlDoc.getElementsByTagName("picname"); |extract all the picture names |

|titles = xmlDoc.getElementsByTagName("title"); |extract all the text |

|count = picnames.length; |determine the number of places |

|var base= xmlDoc.getElementsByTagName("base"); |extract the data for the base map |

|var |extract the lat value |

|baselat=Number(base[0].attributes.getNamedItem("lat").value); | |

|var |extract the lng value |

|baselng=Number(base[0].attributes.getNamedItem("lng").value); | |

|var |extract the number used for the zoom. |

|basesize=Number(base[0].attributes.getNamedItem("size").value)|(I used the word size for the attribute name. I could change this to|

|; |zoom, but leave it to indicate that I can use any name for this |

| |purpose.) |

|var basetype=base[0].attributes.getNamedItem("type").value; |For the future. I chose to just use TERRAIN |

|//not used at present | |

|latlng = new google.maps.LatLng(baselat, baselng); |create the latlng object |

| var myOptions = { |create the myOptions associative array |

| zoom: basesize, | |

| center: latlng, | |

| mapTypeId: google.maps.MapTypeId.TERRAIN, | |

| disableDefaultUI: true |specify the removal of the features to allow player/user to change |

| |the map. |

| }; | |

| map = new |Create map and place in the div |

|google.maps.Map(document.getElementById("map_canvas"), | |

|myOptions); | |

| markertitle = titles[turn].childNodes[0].nodeValue; |Set markertitle for this turn |

| document.f.question.value="Click on "+ markertitle; |Set question |

| latlng=new |Store the lat lng info for this turn. Note need to convert to Number|

|google.maps.LatLng(Number(picnames[turn].attributes.getNamedIt| |

|em("lat").value), | |

|Number(picnames[turn].attributes.getNamedItem("lng").value)); | |

| listener = google.maps.event.addListener(map, 'click', |Set up the event for player action (clicking) |

|function(event) { | |

| checkit(event.latLng) |… to be checkit |

| }); | |

| } |Close initialize |

|function checkit(location) { |Start of checkit function |

| var marker = new google.maps.Marker( |Set up marker for the player's location |

|{position: location, map: map}); | |

| var mk = new google.maps.Marker( |set up marker for the correct position |

|{position: latlng, map: map, | |

|title: markertitle, | |

|icon: picnames[turn].childNodes[0].nodeValue }); | |

|var d=dist(location,latlng); |Call the dist function to calculate the distance |

|var dform=Math.round(d*100)/100 ; |Format to two decimal places |

|document.f.distance.value ="Distance was "+dform+" miles."; |Display the calculated difference (offset) from correct to player |

| |location |

|tots +=d; |Add to tots |

|turn++; |Increment turns |

|if (turn>=count) { |Check if all the questions/places have been asked |

| av = tots / count; |Calculate average |

| av = Math.round(av*100)/100; |format to just 2 decimal places |

| document.f.distance.value =""; |Clear the distance field |

| document.f.question.value = ""; |Clear the question field |

| document.f.average.value = "Average distance for "+count+" |Display message containing the average offset |

|places was "+av+ " miles."; | |

| | |

| google.maps.event.removeListener(listener); |Stop the event handling for clicking |

|} | |

|else { |Still more questions |

| markertitle = titles[turn].childNodes[0].nodeValue; |get text for next question |

| document.f.question.value="Click on "+ markertitle; |... and display the question |

| latlng=new |Set latlng |

|google.maps.LatLng(Number(picnames[turn].attributes.getNamedIt| |

|em("lat").value), | |

|Number(picnames[turn].attributes.getNamedItem("lng").value)); | |

| } |close else |

| } |Close function |

| function dist(point1, point2) { |Start definition of dist function |

| //spherical law of cosines | |

| //var R = 6371; |// This is the value fo kilometers |

| var R = 3959; |// miles |

| var lat1 = point1.lat()*Math.PI/180; |Convert all latitude and longitude values to radians |

| var lat2 = point2.lat()*Math.PI/180 ; | |

| var lon1 = point1.lng()*Math.PI/180; | |

| var lon2 = point2.lng()*Math.PI/180; | |

| | |

|var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) + |Spherical law of cosines |

|Math.cos(lat1)*Math.cos(lat2) * Math.cos(lon2-lon1)) * R; | |

| return d; | |

| } |Close dist function |

| | |

| | |

| |Set up call to initialize |

| |Form used to display (OUTPUT) information, not input. |

| | |

|Next question: | |

| | |

| | |

| | |

| |div to hold map |

| | |

| | |

To make this application your own, after determining the data specifying the region for the map, you need to create an xml file and reference it in the xmlDoc.load call. You need to create all the image files referenced in the xml file. There is much opportunity for improvement; in particular, come up with a better scoring system!

Program 3: Time limit

Timing works in different ways in games. One situation just records the time used. Another situation imposes a time limit. What I wanted for this game was a time limit cutting off play. I then needed to decide what to do if the player did not click on the map in under the specified time. I decided to add a high value to the score and then go on to the next question. I decided to NOT show the correct location. The implementation was fairly easy. My code uses setTimeout to set up a timing event after the specified time. The handler for the event will be the function timeout. (In contrast to setInterval, this sets up a single timing event.) The timeout function, if and when it is called, sets a variable named toolong, initialized to false, to be true. The event handling for the player clicking on the screen remains the function checkit. In the checkit function, the code checks toolong. If toolong is false, the code is as before. If toolong is true, then the high value is added to the score. In all cases, the code in checkit turns the timing event off. If the time out event has occurred, this has no effect. This is a good example of event driven programming.

To summarize, this program adds one function (timeout), and two global variables (toolong and tid), to the previous application. The initialize and the checkit function have minor adjustments.

Here is a screen shot after the player failed to respond to the first question:

[pic]

The code is given in its entirety, but the comments are mainly on the new parts of the code. Please notice the similarities with the previous version.

| | |

| | |

| | |

| | |

| | |

|var map; | |

|var latlng; | |

|var markertitle; | |

|var turn; | |

|var picnames; | |

|var titles; | |

|var count; | |

|var tots = 0; | |

|var listener; | |

|var tid; |Event identifier for timing event |

|var toolong = false; |Boolean used when the timing event does happen, indicating the|

| |player has taken too long |

|function initialize() { | |

| document.f.question.value = ""; | |

| document.f.distance.value = ""; | |

| document.f.average.value = ""; | |

| try //Internet Explorer | |

| { | |

| xmlDoc=new ActiveXObject("Microsoft.XMLDOM"); | |

| } | |

|catch(e) | |

| { | |

| try //Firefox, Mozilla, Opera, etc. | |

| { | |

| xmlDoc=document.implementation.createDocument("","",null); | |

| } | |

| catch(e) | |

| { | |

| alert(e.message); | |

| return; | |

| } | |

| } | |

|xmlDoc.async=false; | |

|xmlDoc.load("game1.xml"); | |

|turn=0; | |

|picnames = xmlDoc.getElementsByTagName("picname"); | |

|titles = xmlDoc.getElementsByTagName("title"); | |

|count = picnames.length; | |

|var base= xmlDoc.getElementsByTagName("base"); | |

|var baselat=Number(base[0].attributes.getNamedItem("lat").value); | |

|var baselng=Number(base[0].attributes.getNamedItem("lng").value); | |

|var basesize=Number(base[0].attributes.getNamedItem("size").value); | |

|var basetype=base[0].attributes.getNamedItem("type").value; //not | |

|used at present | |

|latlng = new google.maps.LatLng(baselat, baselng); | |

| var myOptions = { | |

| zoom: basesize, | |

| center: latlng, | |

| mapTypeId: google.maps.MapTypeId.TERRAIN, | |

| disableDefaultUI: true | |

| }; | |

| map = new google.maps.Map(document.getElementById("map_canvas"),| |

|myOptions); | |

| markertitle = titles[turn].childNodes[0].nodeValue; | |

| document.f.question.value="Click on "+ markertitle; | |

| latlng=new | |

|google.maps.LatLng(Number(picnames[turn].attributes.getNamedItem("la| |

|t").value), | |

| | |

|Number(picnames[turn].attributes.getNamedItem("lng").value)); | |

| listener = google.maps.event.addListener(map, 'click', | |

|function(event) { | |

| checkit(event.latLng); | |

| }); | |

| toolong = false; |Set toolong to false |

| tid = setTimeout("timeout();",4*1000); |Set up the timing event for 4 seconds |

| } | |

| function checkit(location) { | |

| var d; | |

| clearInterval(tid); |Stop the timing event. This is okay even if the event has |

| |happened. |

| if (!toolong) { |If not too long, that is, if the event didn't happen, proceed |

| |with normal processing |

| var marker = new google.maps.Marker( | |

|{position: location, map: map }); | |

| var mk = new google.maps.Marker( | |

|{position: latlng, map: map, | |

|title: markertitle, icon: picnames[turn].childNodes[0].nodeValue| |

|}); | |

| d=dist(location,latlng); | |

| var dform=Math.round(d*100)/100 ; | |

| document.f.distance.value = "Distance was "+dform+" miles.";} | |

|else { |Player took too long |

| d = 100; } |assign penalty of 100 |

|tots +=d; | |

|turn++; | |

|if (turn>=count) { | |

| av = tots / count; | |

| av = Math.round(av*100)/100; | |

| document.f.distance.value =""; | |

| document.f.question.value = ""; | |

| document.f.average.value = | |

|"Average distance for "+count+" places was "+av+ " miles."; | |

|google.maps.event.removeListener(listener); | |

| } | |

|else { | |

| markertitle = titles[turn].childNodes[0].nodeValue; | |

| document.f.question.value="Click on "+ markertitle; | |

| latlng=new | |

|google.maps.LatLng(Number(picnames[turn].attributes.getNamedItem("la| |

|t").value), | |

| | |

|Number(picnames[turn].attributes.getNamedItem("lng").value)); | |

| toolong = false; |set toolong to false |

| tid = setTimeout("timeout();",4*1000); |set up timing event for next question |

| } | |

| } | |

| function timeout() { |start of timeout function |

| document.f.distance.value = "TOOK TOO LONG: 100 miles charged."; |Display message |

| toolong = true; |Set toolong to true |

| checkit(null); |Call checkit function |

| } | |

| function dist(point1, point2) { |//spherical law of cosines |

| //var R = 6371; |// km |

| var R = 3959; |// miles |

| var lat1 = point1.lat()*Math.PI/180; | |

| var lat2 = point2.lat()*Math.PI/180 ; | |

| var lon1 = point1.lng()*Math.PI/180; | |

| var lon2 = point2.lng()*Math.PI/180; | |

|var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) + | |

|Math.cos(lat1)*Math.cos(lat2) * Math.cos(lon2-lon1)) * R; | |

| return d; | |

| } | |

| | |

| | |

| | |

| | |

| | |

|Next question: | |

| | |

| | |

| | |

| | |

| | |

| | |

Program 4: Random selection of questions, button for stopping, degrees and minutes encoding of latitude and longitude

The random selection of questions is done in the usual way:

turn = Math.floor(Math.random()*count);

It is necessary in this case to keep a count of the questions answered. I used a variable cnt.

The button for stopping is produced using a element of type submit in the form used to hold the information displayed to the player. The onsubmit action is to invoke a function called done. The code in done is essentially the code found in previous examples when the value of the turn variable was at the count of questions.

Someone requested a game for cities in New York State. I found a list (though I do not believe it is complete) of the places in New York State. One problem was that the latitude and longitude was given as 42° 45' N and 73° 48' W. PLEASE NOTE: the best thing to do in these circumstances was to convert all the data ahead of tiem to the best format for my application and not leave it to be done at runtime. However, I decided to make my program be able to handle either formats, namely to check if the text indicating latitude or longitude was a number or something else. If it was something else, the code searches for the ° symbol and then for the ' symbol. It also checks for the presence of a "S" or a "W" and negates the number if either is found. Modifying the code was easy. Where the coding in the prior examples made a call to the built-in function Number to convert text to number, I put in a call to my own function getcoord. The getcoord function uses the built-in isNaN to determine that a string is not a number. If this is the case, it uses indexOf to find each of the degree and minute symbols and then substring to extract that part of the original text. The final answer is done by converting each part to a number and dividing the minute part by 60.

function getcoord(v) {

var res;

if (isNaN(v)) {

degi = v.indexOf('°');

seci = v.indexOf("'");

p1 = v.substring(0,degi);

p2 = v.substring(degi+2,seci);

res=Number(p1)+Number(p2)/60;

if ((v.indexOf("S")>-1)||(v.indexOf("W")>-1)) {

res = -res;

}

}

else {

res = Number(v);

}

return res;

}

It would make sense to use this approach if your application accesses information provided by others and you cannot guarantee the format you want.

The xml file used for this program starts off:

New York

blob.png Albany AP (S)

blob.png Albany Co

blob.png Auburn

blob.png Batavia

By the way, I manipulated the original list by constructing a table in Word, inserting columns with the constant content such as ................
................

In order to avoid copyright disputes, this page is only a partial summary.

Google Online Preview   Download