The preceding illustration reveals a second problem with the DISTAL system: because the USA including Alaska is over 4,000 miles wide, accurately selecting a 10-mile search radius by clicking on a point on this map would be an exercise in frustration.
To solve this problem, we will implement a zoom feature so that the user can click more accurately on the desired starting point. Because the DISTAL system is implemented as a series of CGI scripts, our zoom feature is going to be rather basic: if the user holds down the Shift key while clicking, we zoom in on the clicked-on point. If the Shift key is not held down when the user clicks, we proceed with the search as usual.
In a real web application, we would implement a complete slippy map interface that supports click-and-drag as well as on-screen controls to zoom both in and out. Doing this is way beyond what we can do with simple CGI scripts, however. We will return to the topic of slippy maps in Chapter 10, Tools for Web-based Geospatial Development.
To implement zooming, we will need to update the selectArea.py script we wrote earlier. We will make use of some rudimentary JavaScript code to detect whether the user held down the Shift key and, if so, reload the "select area" page with additional CGI parameters that allow us to zoom in. In particular, we are going to modify our CGI script to accept the following additional parameters:
|
CGI parameter(s) |
Description |
|---|---|
|
|
The coordinates of the point on which the user clicked. |
|
|
The zoom level to use, where |
|
|
The lat/long bounding box that was used to draw the map at the previous zoom level. |
All of these parameters are optional—if they are not supplied, default values will be calculated.
Let's modify our script to use these parameters to zoom in on the map. Start by deleting the block of code that calculated the lat/long bounding box, that is, the lines of code starting with the cursor.execute("SELECT name, ...") statement and ending with the line maxLat = maxLat + 0.2. Replace these with the following code:
if "x" in form:
click_x = int(form['x'].value)
else:
click_x = None
if "y" in form:
click_y = int(form['y'].value)
else:
click_y = None
if "zoom" in form:
zoom = int(form['zoom'].value)
else:
zoom = 0
if ("minLat" in form and "minLong" in form and
"maxLat" in form and "maxLong" in form):
# Use the supplied bounding box.
minLat = float(form['minLat'].value)
minLong = float(form['minLong'].value)
maxLat = float(form['maxLat'].value)
maxLong = float(form['maxLong'].value)
else:
# Calculate the bounding box from the country outline.
cursor.execute("SELECT " +
"ST_YMin(ST_Envelope(outline))," +
"ST_XMin(ST_Envelope(outline))," +
"ST_YMax(ST_Envelope(outline))," +
"ST_XMax(ST_Envelope(outline)) " +
"FROM countries WHERE id=%s", (countryID,))
row = cursor.fetchone()
if row != None:
minLat = row[0]
minLong = row[1]
maxLat = row[2]
maxLong = row[3]
else:
print(HEADER)
print('<b>Missing country</b>')
print(FOOTER)
sys.exit(0)
minLong = minLong - 0.2
minLat = minLat - 0.2
maxLong = maxLong + 0.2
maxLat = maxLat + 0.2
# Get the country's name.
cursor.execute("SELECT name FROM countries WHERE id=%s",
(countryID,))
name = cursor.fetchone()[0]As you can see, we extract the x, y, and zoom CGI parameters, setting them to default values if they are not supplied. We then check whether the minLat, minLong, maxLat, and maxLong parameters are supplied. If not, we ask the database to calculate these values based on the country's outline and then add a margin of 0.2 degrees to each one. Finally, we load the name of the country from the database.
Now that we have our CGI parameters and are still calculating default values when required, we can use these parameters to zoom in on the map as required. Fortunately, this is quite easy: we simply modify the bounding box to show a smaller area of the world if the user has zoomed in. To do this, add the following code immediately before the hilite = "[id] = " + str(countryID) line:
if zoom != 0 and click_x != None and click_y != None:
xFract = float(click_x)/float(mapWidth)
longitude = minLong + xFract * (maxLong-minLong)
yFract = float(click_y)/float(mapHeight)
latitude = minLat + (1-yFract) * (maxLat-minLat)
width = (maxLong - minLong) / (2**zoom)
height = (maxLat - minLat) / (2**zoom)
minLong = longitude - width / 2
maxLong = longitude + width / 2
minLat = latitude - height / 2
maxLat = latitude + height / 2In this section of the script, we first calculate the latitude and longitude of the point where the user clicked. We will zoom in on this point. We then use the zoom level to calculate the width and height of the area to display, and we then recalculate the bounding box so that it has the given width and height but is centered on the clicked-on point.
We next need to write the JavaScript code to detect when the user clicks with the Shift key held down, and call our script again with the appropriate parameters. To do this, replace the HEADER definition near the top of the file with the following:
HEADER = "\n".join([
"Content-Type: text/html; charset=UTF-8",
"",
"",
"<html><head><title>Select Area</title>",
"<script type='text/javascript'>",
" function onClick(e) {",
" e = e || window.event;",
" if (e.shiftKey) {",
" var target = e.target || e.srcElement;",
" var rect = target.getBoundingClientRect();",
" var offsetX = e.clientX - rect.left;",
" var offsetY = e.clientY - rect.top;",
" var countryID = document.getElementsByName('countryID')[0].value;",
" var minLat = document.getElementsByName('minLat')[0].value;",
" var minLong = document.getElementsByName('minLong')[0].value;",
" var maxLat = document.getElementsByName('maxLat')[0].value;",
" var maxLong = document.getElementsByName('maxLong')[0].value;",
" var zoom = document.getElementsByName('zoom')[0].value;",
" var new_zoom = parseInt(zoom, 10) + 1;",
" window.location.href = 'selectArea.py'",
" + '?countryID=' + countryID",
" + '&minLat=' + minLat",
" + '&minLong=' + minLong",
" + '&maxLat=' + maxLat",
" + '&maxLong=' + maxLong",
" + '&zoom=' + new_zoom",
" + '&x=' + offsetX",
" + '&y=' + offsetY;",
" return false;",
" } else {",
" return true;",
" }",
" }",
"</script>",
"</head><body>"])We won't go into the details of this code, as writing JavaScript is beyond the scope of this book. In this JavaScript code, we define a function called onClick() which checks to see whether the user held down the Shift key and, if so, calculates the various CGI parameters and sets window.location.href to the URL used to reload the CGI script with those parameters.
There are just two more things we need to do to complete our new zoom feature: we need to add a new hidden form field to store the zoom value, and we need to call our onClick() function when the user clicks on the map. Add the following line to the list of print(HIDDEN_FIELD.format(...)) statements near the bottom of the script:
print(HIDDEN_FIELD.format("zoom", zoom))Finally, change the print('<input type="image"...>') line to the following:
print('<input type="image" src="' + imgFile + '" ismap ' +
'onClick="return onClick()">')This completes the changes required to support zooming. If you now run the DISTAL program and select a country, you should be able to hold down the Shift key while clicking to zoom in on the map. This should make it much easier to select the desired point within a larger country.
We have now corrected the two major usability issues in the DISTAL system. There is, however, one other area we need to look at: performance. Let's do this now.