Technetra

Archive for April, 2009

Toward Cleaning Up Out-of-Date or Broken Examples In The GNU Bash Project

Monday, April 27th, 2009

I had great fun putting together the tutorial Discovering Web Access Latencies Using Bash Co-Processing. However, speaking about examples that demonstrate the fabulous features of GNU Bash

Bash is an important and even critical software project. Yet it’s a shame that the project does not always keep its examples and illustrations up to date or even accurate. The project should strive to showcase Bash’s latest features in an authoritative way. And it should prune any items that are no longer relevant or that don’t actually work. A case in point involves the coproc examples located in the bash-4.0/examples/functions directory. Now that there is support for rudimentary (i.e., single instance) coprocessing in Bash4, the old coproc examples should be updated with native implementations.

But wait, that’s not all. There are at least two problems with the illustration of coprocessing as currently shown in the GNU Bash project files. As seen in the coshell.README file, the synopsis for using the coproc.bash example function is:

Figure 1. Excerpt from bash-4.0/examples/functions/coshell.README (DOES NOT WORK)

...
Attached to the message you will find coprocess.bash and coshell.bash.
Here's a brief synopsis of use:

coprocess open telnet localhost
while coprocess read il ; do # ← PROBLEM 1
  echo "$il"
  case "$il" in
    *ogin:*)
      coprocess print 'user'
      ;;
    *ord:*)
      echo 'pass' |coprocess print --stdin
      ;;
    *$ *) # ← PROBLEM 2
      coprocess print 'exit'
      break
      ;;
  esac
done
coprocess close
...

The first problem with this example is that the Bash builtin read command, which coprocess read eventually calls, is by default in line mode which means that it normally won’t release characters for consumption until it receives a carriage return. Therefore, in this illustration, characters that are not released by a carriage return will not be seen by the body of the case statment and consequently the desired login and password responses will not be executed. A fix for this problem requires restructuring the program (see Figure 2 below).

The second problem with this illustration is in the case statement itself. The ‘*$ *’ pattern produces a syntax error because the space in the pattern needs to be escaped. This has been fixed in the version below.

Figure 2. A corrected version of the coprocessing excerpt from bash-4.0/examples/functions/coshell.README

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/bin/bash
 
. ./coproc_orig.bash
 
coprocess open telnet localhost
# initial read mode: blocks until read sees ':'
# (responds to Login and Password prompts which
#  are normally followed by ': ', not carriage returns)
while coprocess read -d: il ; do
  echo "$il"
  case "$il" in
    *ogin*)
      coprocess print 'lizbennett'
      ;;
    *ord*)
      echo 'pride&prejudice' |coprocess print --stdin
      break
      ;;
  esac
done
coprocess print 'exit'
# next read mode: blocks until read sees carriage return
while coprocess read il ; do
  echo "$il"
  case "$il" in
    # just logs off at echo of first prompt
    *$\ exit)
      break
      ;;
  esac
done
coprocess close

Now that we’ve fixed the example of the old approach to coprocessing, let’s put together some code to demonstrate the new way. The following is an updated — and correct :) — illustration of coprocessing as supported natively in Bash4. This example uses the new coproc command. It also handles user name and password input. In this example coproc invokes telnet to remotely execute a command given as a command line argument. The program can also run a stream of commands provided on stdin. Enjoy!

Figure 3. Native Coprocessing Example Using Bash 4.0 coproc Command

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/bin/bash4
# Edit above line to point to where your Bash 4 executable lives
# e.g., /bin/bash if you are running a distribution like Fedora 11
# that supports the latest version of the shell
#
# Examples:
#
#     Save this program as test_native_coproc.sh in the current directory,
#     then run chmod +x test_native_coproc.sh,
#     and then execute commands like the following:
#
#     ./test_native_coproc.sh date
#     echo -e "date\nuname -r\nwho" | ./test_native_coproc.sh -i -u darcy -p secret
#
#######################################################################################
#
# process command line arguments
OPTIND=1
while getopts "ip:u:" c; do
  case $c in
    i)	USE_STDIN=y ;;
    p)	MYPASSWORD=$OPTARG ;;
    u)	MYUSER=$OPTARG ;;
  esac
done
shift $(( $OPTIND - 1 ))
ARGS="$@"
MYUSER=${MYUSER:-lizbennett}
MYPASSWORD=${MYPASSWORD:-pride&prejudice}
 
# set up telnet coprocess, log in and then execute a single command
# or stream of commands if using stdin
coproc telnet localhost
while read -d: -u ${COPROC[0]}; do
  echo "$REPLY"
  case "$REPLY" in
    *ogin)
      echo "$MYUSER" >&${COPROC[1]}
      ;;
    *ord)
      echo "$MYPASSWORD" >&${COPROC[1]}
      break
      ;;
  esac
done
case $USE_STDIN in
  y)
    cat >&${COPROC[1]}
    ;;
  *)
    echo "$ARGS" >&${COPROC[1]}
    ;;
esac
echo "exit" >&${COPROC[1]}
while read -u ${COPROC[0]}; do
  echo "$REPLY"
done

Discovering Web Access Latencies Using Bash Co-Processing

Sunday, April 26th, 2009

This tutorial describes a Bash co-processing script latency.sh (Listing 1) that generates and sorts web access latencies for a user supplied list of hosts. It demonstrates a co-processing scheme that supports multiple, simultaneous co-processes and works in all versions of Bash.

It is an updated and parallelized version of an earlier script with a similar purpose, ffmirror.sh (see http://grulos.blogspot.com). This new script uses customized Bash co-processing support for parallelization. In addition, unlike the grulos script, this script does not require a Bash executable that has been compiled for special network redirection (that is, with compiled-in support for /dev/tcp, etc.). You can use either tcp redirection or wget by specifying the desired mode as an argument on the command line. This is good news for Debian and Ubuntu users because these distributions normally do not ship with net redirections enabled. Note that while the Bash net redirection mode is a bit faster than launching and using wget, the latter may be more robust across many iterations of running the script against large lists of both existing and non-existent hosts.

Co-processing can reduce the performance profile of the principal work path of the script from linear, or O(n), to nearly O(1). The principal work path in our case is setting up host connections. In our implementation, collecting the latency results is still linear, but with a negligible run-time coefficient.

latency.sh can be run with the default wget connection strategy:

$ ./latency.sh < myhostlist

or with the Bash network redirection strategy where available:

$ ./latency.sh tcp < myhostlist

Using the 20-entry sample host list at grulos.blogspot.com, the time to run this script on a normal desktop can be under 2 seconds, while the original code requires over 18 seconds. This makes it feasible to run such a command under programs like /usr/bin/watch to create dynamically updating displays.

Figure 1: Trial run (using the original sample host data from grulos.blogspot.com)

# time ./latency.sh tcp < new_hostlist.txt
04/26/2009 19:40:49 running tcp version
270ms www.google.com
540ms www.redhat.com
900ms www.kernel.org
940ms www.microsoft.com
940ms www.slackware.org
940ms www.w3.org
960ms www.ibm.com
990ms www.debian.org
1000ms www.wikipedia.org
1000ms www.gentoo.org
1050ms www.altavista.com
1070ms www.yahoo.com
1070ms www.dell.com
1090ms www.netbsd.org
1100ms www.freebsd.org
1220ms www.openbsd.org
1860ms www.linux.org
(not set) www.shakakalasfdksasdfasdf.com
(not set) www.oooooooos.com
(not set) www.tttttttttt.org

real	0m1.982s
user	0m0.156s
sys	0m0.160s

Note that hosts whose latencies cannot be computed, for whatever reason, are labelled as ‘(not set)’.

The Bash co-processing function presented in Listing 2 below for coprocess.bash is based on a similar example from the GNU Bash distribution. The difference is that this co-processing function implements a simple queuing facility to handle multiple simultaneous coprocesses. This is essential for us to be able to parallelize our web access application. It also uses parameterized named pipes (fifos) for communicating between coprocesses.

The script latency.sh works under both Bash 3.x and Bash 4.0. It should be noted that the recent native support for co-processes in Bash 4.0 is limited to a single co-process at a time and so could not meet our need for multiple simultaneous web connections.

Listing 1: latency.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/bin/bash
#
# latency.sh - Generate and sort web access latencies
#              for a list of hosts
#
# Copyright (c) 2009 Technetra Corp
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
# 
# This is an updated and parallelized version
# of ffmirror.sh (http://grulos.blogspot.com)
#
# To run the default wget based version:
#    ./latency.sh < hostlist.txt
# or for the tcp (Bash net redirection) version:
#    ./latency.sh tcp < hostlist.txt
#
###################################################################################
 
. ./coprocess.bash
 
WHICH_BACKGROUNDER=${1:-wget}
echo "$(date '+%m/%d/%Y %H:%M:%S') running $WHICH_BACKGROUNDER version"
 
chk_time () {
  local t=$(</proc/uptime)
  t=${t%% *}
  t=${t/./}
  eval $1=$t
}
 
wget_backgrounder () {
  local host=$1 a b
  [[ -z "$host" ]] && read host
  chk_time a
  wget --spider -T 10 -q -O /dev/null $host && chk_time b
  echo "$((b-a))|$host"
}
 
tcp_backgrounder () {
  local host=$1 a b
  [[ -z "$host" ]] && read host
  chk_time a
  exec 2>/dev/null 3<>/dev/tcp/$host/80 &&
    # send a GET request and read 1st line of response
    printf "GET / HTTP/1.1\r\nHost: $host\r\n\r\n">&3 &&
    read -u 3 -t 10 -a response &&  exec 3>&- && chk_time b
  echo "$((b-a))|$host"
} 
 
# spin off parallel connections (the 'map' in 'MapReduce')
while read hostname; do
  # provide hostname to wget or /dev/tcp as command-line argument
  coprocess open qindex ${WHICH_BACKGROUNDER}_backgrounder "$hostname"
  # alternatively, send hostname to wget or /dev/tcp via pipe:
  #   coprocess open qindex wget_backgrounder
  #   coprocess put $qindex "$hostname"
done
 
# collect and process results (the 'reduce' in 'MapReduce')
coprocess size len
for ((i=0;i<$len;i++)); do
  # read result from pipe
  coprocess get $i c
  coprocess close $i
  h=${c##*|}
  d=${c%%|*}
  if [[ d -ge 0 ]]; then
    # bucket sort variant
    e=$((d*100))
    until [ "${slist[e]}" == "" ]; do
      ((e++))
    done
    slist[e]="${d}0ms $h" 
  else
    elist[not_set++]="(not set) $h"
  fi
done
 
printf "%sn" "${slist[@]}"
[[ ${#elist[*]} -ge 0 ]] &&
  printf "%sn" "${elist[@]}"

Listing 2: coprocess.bash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# coprocess.bash
#
#    Copyright (c) 2009 Technetra Corp
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###################################################################################
 
declare -i FIFO_IN FIFO_OUT FIFO_START=60
declare -a COPROCESSQ
 
coprocess () {
  local CMD="$1" N="$2"
  shift 2
  [[ "$N" == [[:digit:]]* ]] && 
      FIFO_IN=$((N*2+FIFO_START)); FIFO_OUT=$((FIFO_IN+1))
 
  case "$CMD" in
 
    open) 
      local M=${#COPROCESSQ[*]}
      FIFO_IN=$((M*2+FIFO_START)); FIFO_OUT=$((FIFO_IN+1))
      local fifo="/var/tmp/coprocess.$FIFO_IN.$FIFO_OUT.$$.$RANDOM"
 
      mkfifo "$fifo.in" || return $?
      mkfifo "$fifo.out" || {
        ret=$?
        rm -f "$fifo.in"
        return $ret
      }
 
      ( trap "rm -f $fifo.in $fifo.out" 0; "$@" <$fifo.in >$fifo.out ; ) &
      COPROCESSQ[M]=$fifo
      eval "$N=$M"
      eval "exec $FIFO_IN>$fifo.in $FIFO_OUT<$fifo.out"
      return 0
      ;;
 
    close)
      eval "exec $FIFO_IN>&- $FIFO_OUT<&-"
      [ "$1" = "-SIGPIPE" ] && return 1
      return 0
      ;;
 
    get)
      local old_trap=$(trap -p SIGPIPE)
      trap "coprocess close $N -SIGPIPE" SIGPIPE
      builtin read "$@" <&$FIFO_OUT
      local ret=$?
      eval "$old_trap"
      return $ret
      ;;
 
    put)
      local old_trap=$(trap -p SIGPIPE)
      trap "coprocess close $N -SIGPIPE" SIGPIPE
      builtin echo "$@" >&$FIFO_IN
      local ret=$?
      eval "$old_trap"
      return $ret
      ;;
 
    size)
      local M=${#COPROCESSQ[*]}
      eval "$N=$M"
      ;;
 
  esac
}

How to Setup Ruby on Rails for Fedora 10 and 11

Wednesday, April 22nd, 2009

In this how-to, we will step through the process of setting up a Ruby on Rails development environment on Fedora 10 and Fedora 11 Beta. To follow along, just cut and paste the commands in a terminal window.

The instructions in this how-to are based on a fresh installation of Fedora 10 (32-bit/64-bit) and Fedora 11 Beta (32-bit only). During the installation process, we selected the “Software Development” package which includes the tools (e.g., gcc, svn, git, make) and libraries needed for building and compiling software.

1. Development tools

First, let’s check to see if you have the basic set of development tools — gcc, make and git. Although these tools are installed as part of the “Software Development” package, you can check if they are on your system by running ‘which gcc make git‘ at the command line.

[root@fc10 ~]# which gcc make git
/usr/lib/ccache/gcc
/usr/bin/make
/usr/bin/git

Note: On Fedora 10 64-bit systems, the full path to gcc is /usr/bin/gcc.

If the tools are not on your system, then you can install them by running the following command.

[root@fc10 ~]# yum install gcc make git

2. Databases: MySQL, SQLite

A typical Rails application is backed by a relational database. We will install both MySQL (v5.0.77) and SQLite (v3.5.9) for our environment. Recent versions of Rails default to using SQLite. If set up on your system, however, MySQL is also easily used.

[root@fc10 ~]# yum install mysql-server mysql-libs mysql-devel
[root@fc10 ~]# yum install sqlite sqlite-devel

3. Ruby language

Next, we’ll install the core packages for Ruby (v1.8.6.287). We’ll also need the two packages ruby-mysql and ruby-sqlite3 for database access from Ruby programs.

[root@fc10 ~]# yum install ruby ruby-devel ruby-libs ruby-mode ruby-rdoc ruby-irb ruby-ri ruby-docs ruby-mysql ruby-sqlite3

4. RubyGems

RubyGems is the packaging system for Ruby applications and libraries. Each application or library is known as a gem. By default, gems are installed in ‘/usr/lib/ruby/gems‘ (’/usr/lib64/ruby/gems‘ on Fedora 10 64-bit systems). RubyGems lets you easily install and remove gems using the gem command. We’ll install the latest version of RubyGems (v1.3.2).

[root@fc10 ~]# wget -q http://rubyforge.org/frs/download.php/55066/rubygems-1.3.2.tgz
[root@fc10 ~]# tar xzf rubygems-1.3.2.tgz
[root@fc10 ~]# cd rubygems-1.3.2
[root@fc10 rubygems-1.3.2]# ruby setup.rb

5. Rails

Now that we have a sparkling RubyGems toolset, the first gem we are going to install is the Rails (v2.3.2) web application framework. An advantage of installing Rails using the gem command is that package dependencies are resolved automatically.

[root@fc10 ~]# gem install rails

6. Mongrel

Rails provides WEBrick, a simple HTTP server that can be used to test applications during development. However, WEBrick is not suited for production environments. Mongrel is a web server commonly used for Ruby web applications. A typical configuration consists of a Mongrel cluster (several Mongrel instances) behind a front-end Apache web server. We’ll install Mongrel 1.1.5.

[root@fc10 ~]# gem install mongrel mongrel_cluster

7. JSON

JSON (JavaScript Object Notation) is a lightweight data interchange format - a sort of “low-fat” alternative to XML. We’ll install the Ruby implementation of the JSON specification, json (v1.1.4). This allows the conversion of Ruby objects to JavaScript objects for use by clients running on the browser.

[root@fc10 ~]# gem install json

8. SQLite3 Ruby

The sqlite3-ruby gem provides Ruby bindings for SQLite3 databases, the default database system for recent Rails environments. We’ll install version 1.2.4.

[root@fc10 ~]# gem install sqlite3-ruby

9. Ruby MySQL

The mysql gem provides a client-side MySQL API for Ruby programs. Before we install the gem, we’ll need to create symbolic links in ‘/usr/lib‘ (’/usr/lib64‘ on Fedora 10 64-bit systems) to point to the MySQL client libraries installed in ‘/usr/lib/mysql‘ (’/usr/lib64/mysql‘ on Fedora 10 64-bit systems).

Fedora 10

[root@fc10 ~]# cd /usr/lib
[root@fc10 lib]# ln -s mysql/libmysqlclient.so
[root@fc10 lib]# ln -s mysql/libmysqlclient.so.15
[root@fc10 lib]# ln -s mysql/libmysqlclient_r.so
[root@fc10 lib]# ln -s mysql/libmysqlclient_r.so.15

Note: On Fedora 10 64-bit systems, the symbolic links should be created in ‘/usr/lib64‘.

Fedora 11 Beta

[root@fc11 ~]# cd /usr/lib
[root@fc11 lib]# ln -s mysql/libmysqlclient.so
[root@fc11 lib]# ln -s mysql/libmysqlclient.so.16
[root@fc11 lib]# ln -s mysql/libmysqlclient_r.so
[root@fc11 lib]# ln -s mysql/libmysqlclient_r.so.16

Or for the brave, just run this on either Fedora 10 or 11:

[root@fc10 ~]# cd /usr/lib
[root@fc10 lib]# for f in mysql/libmysql*.so mysql/libmysql*.so.1[56]; do ln -s $f; done

Note: On Fedora 10 64-bit systems, the symbolic links should be created in ‘/usr/lib64‘.

Now, we are ready to install the Ruby MySQL (v2.7) gem by running the following command.

[root@fc10 ~]# gem install mysql

10. Riding the Rails

Now it’s time to test your installation. A great way to do this is to follow the simple tutorial at ‘Getting Started with Rails‘. Build the blogging application described there twice: once for SQLite3 and then again for MySQL. If the application can be built without errors, your database environments are set up properly.

That’s it! Your shiny new environment is waiting for you to build your next Rails application on Fedora using SQLite and MySQL.

Tutorial: Comparing Yahoo! and Google Maps APIs Side-by-Side

Sunday, April 19th, 2009

This tutorial compares the APIs of Yahoo! and Google maps by implementing a simple side-by-side map overlay rendered on a single HTML page. In this tutorial we will describe the details of using each API in JavaScript. Along the way we’ll compare a few of the differences between the two APIs. This material is suitable to be incorporated as part of a computer science lab covering the AJAX interfaces to the Yahoo and Google Maps APIs.

Both the Yahoo! and Google Maps APIs are easy to work with. The packages are similar. Both started out being based on mapping technology provided by Navteq. Today Google uses Tom Tom’s Tele Atlas data, while Yahoo has continued to use Navteq. But both companies source geographic data from a variety of other partners as well. The Google Maps API seems to support a few more features, especially in navigation and map types. In addition, the Google maps generally offer greater detail and more up-to-date content, especially when rendering international regions.

In order to compare both packages side-by-side we initially wanted to run each Maps API together on a single page. Unfortunately, we could not load both APIs together due to namespace collisions. A workaround was to load each package into its own iframe.

With this approach we were able to develop a web page that shows an initial default geographic location, the Golden Gate Bridge, San Francisco, side-by-side in each of the Maps APIs. Each map is rendered with a sample of its package’s standard navigation controls. On each map, a marker is placed at the center when the map is initially rendered. On a MouseOver event, this marker will display a pop-up picture of the target location, which in our case is the Golden Gate Bridge. The pop-up image will also provide a link to its associated Wikipedia entry.

The user can specify a new address for each map and is also be able to input different latitude and longitude coordinates to render a new map location. In addition, a fresh marker is placed anywhere on the map that the user clicks.

Figure 1: Comparing Maps APIs Side-by-Side — Screenshot

Figure 1: Comparing Maps APIs Side-by-Side — Screenshot

Try this example

Starting out with Yahoo! and Google Maps APIs

An introduction to the AJAX Maps API for Yahoo! can be found at Yahoo! Maps Web Services - AJAX API Getting Started Guide. An introduction to the Maps API for Google can be found at Google Maps API.

You must obtain your own Yahoo! app ID and Google key in order to run the application presented in this tutorial on your own server.

Overview of Our Application

Our application resides in an HTML page that contains two iframe elements. The JavaScript application in each iframe can call functions in the other iframe. This feature is used to synchronize the two maps. Cross-frame communication is limited to using simple numeric and string values as arguments to the method calls; complex parameters such as objects cannot be supported.

Figure 1: Comparing Maps APIs Side-by-Side — Overview

Figure 2: Comparing Maps APIs Side-by-Side — Overview

Parent Page

IFRAME layout

The parent page that defines the iframe layout for our application, as depicted in Figure 1, is shown in the following listing:

Listing 1: Parent page with iframe layout

<!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>Side-by-Side Yahoo! and Google Maps</title>
  </head> 
  <body> 
    <div style="width: 844px; background-color: #ccc;"> 
      <br />
      <h2 style='text-align: center; color: white;'>
        Side-by-Side  Yahoo! and Google Map APIs
      </h2>
      <br />
    </div>
    <div style="width: 844px;">
      <iframe src="yahoo_map_frame.html" name="yahoo"
              style="border: none; width: 420px; height: 800px; position: absolute;">
      </iframe>
      <iframe src="google_map_frame.html" name="google"
              style="border: none; width: 420px; height: 800px; float: right;">
      </iframe>
    </div>
  </body>
</html>

Map Application Walkthrough

In this section we will explain various details about how our application is constructed and how it uses each Maps API package. Full listings for each API solution are presented at the end of the tutorial.

Loading the Maps API

Each iframe loads its own Maps API package. The following <script> tag loads the Google Maps API by supplying the required key and sensor flag values. The sensor flag notifies Google about whether a sensor is being used to determine the user’s location.

Listing 2: Loading the Google Maps API package

<!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>
    <script src="http://maps.google.com/maps?file=api&v=2&key=[YOUR GOOGLE MAPS API KEY]&sensor=[Are you using a location sensor? true or false]"></script>
    <script>
...

The Yahoo! Maps API is loaded in a similar manner but doesn't require the sensor notification parameter.

Listing 3: Loading the Yahoo! Maps API package

...
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <script src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=[YOUR YAHOO! MAPS API KEY]"></script>  
...

Setting Up Globals

After loading the Google or Yahoo! Maps API, each map application sets up a small number of global variables including defaultGeoTargets, $EL, and myMap.

The global variable defaultGeoTargets holds information about the features we want to highlight on the map. It is an array of hash literals containing address, geolocation and html values for use by the popup information windows associated with map markers.

Three well known landmarks will be featured on our map, the Golden Gate Bridge in San Francisco, the Hallepoort Gate in Brussels and the ancient Ile de la Cite of Paris. The literal array (of hash literals) assigned to defaultGeoTargets can easily be extended to add more landmarks. Alternatively, other data sources such as JSON could be used to populate the defaultGeoTargets array.

Listing 4: defaultGeoTargets global function literal

...
  <script>
    var defaultGeoTargets = [
      {
        address: 'Golden Gate Bridge, San Francisco',
        geobox: { N:37.833, E:-122.474, S:37.805, W:-122.482 },
        html: "<a href='http://en.wikipedia.org/wiki/Golden_Gate_Bridge' target='_blank'><img src='/images/sf_bridge_small.jpg' width='160px' height='120px'></a>"
      },
      {
        address: 'Hallepoort, Brussels',
        geobox: { N:50.834, E:4.345, S:50.833, W:4.344 },
        html: "<a href='http://en.wikipedia.org/wiki/Halle_Gate' target='_blank'><img src='/images/hallepoort_1612.jpg' width='120px' height='160px'></a><span style='position: absolute;'><p>&nbsp;Hallepoort<br />&nbsp;in 1612.</p></span>"
      },
      {
        address: 'Ile de la Cite, Paris',
        geobox: { N:48.86, E:2.349, S:48.85, W:2.341 },
        html: "<a href='http://en.wikipedia.org/wiki/Ile_de_la_Cite' target='_blank'><img src='/images/ile_de_la_cite.jpg' width='200px' height='150px'></a>"
      }
    ];
...

The second global variable $EL is simply a convenience function for easily accessing the client JavaScript function document.getElementById().

Listing 5: $EL convenience function

...
    var $EL = function(id) { return document.getElementById(id); };
...

Our Application Encapsulated as a JavaScript Function Literal

The third and last of our global variables myMap holds our principal data structure for interfacing with the specific Maps API we have selected, Google or Yahoo!. It uses the technique of JavaScript function closure to isolate its namespace, retain state, and to export public methods to its caller. The public methods are contained in an associative array object returned and assigned to myMap. The public methods exported include .initMap, .showMap, .reMap, .remoteRemap and .changeAddress. All other methods and attributes within the closure context are effectively private. Note that the variable myMap is assigned the result of executing an anonymous function literal immediately after it has been read in by the browser (notice the trailing parentheses after the function literal definition). The function literal then sets up the closure context and returns the associative array of public function literals.

Listing 6: How JavaScript builds the myMap function closure and returns a hash of public functions

...
    var myMap = function() {
      // function closure environment
      //   private attributes
      //   private interface adapters
      //   private application functions
      // return {
      //   hash of public functions
      // }(); <--- parentheses cause immediate execution of function literal
    }
...

Private Attributes

The function literal assigned to myMap starts out by defining several private variables including map and targetFeatures. These private variables are retained as attributes of the function closure context. In our application, they are initialized by calling the public function .initMap from the onLoad event attribute of the HTML body element. The private variable map will hold the basic map structure allocated by the selected underlying Maps API. The variable targetFeatures will be assigned a singleton associative array that contains the proximity function, .isNearby which we will examine in detail in the section Private Application Functions below. The variable DONT_PAN is used as a self-documenting constant for requesting that the panAndMark function temporarily turn off map panning. We will look at the function panAndMark in a moment.

Listing 7: The private attributes of myMap's function closure

...
    var myMap = function() {
      var map;
      var targetFeatures;
      var DONT_PAN = true;
...

Private Interface Adapters

The next section of myMap defines wrappers around the underlying Maps APIs for Yahoo and Google. These functions are adapted to each API.

The following table shows the relationship between our private interface adapters and the corresponding functions in the Yahoo! and Google packages.

Table 1: Wrappers for selected Maps API calls

Private Interface Adapter Yahoo! Maps API Google Maps API
allocMap new YMap new GMap2
applyUIControls addTypeControl, addZoomLong, addPanControl setUIToDefault, addControl
browserIsCompatible return YMAPPID GBrowserIsCompatible
captureEvent YEvent.Capture GEvent.addListener
createMarker new YMarker new GMarker
eraseOverlays removeMarkersAll clearOverlays
getZoomLevel getZoomLevel getZoom
makePointFromFields new YGeoPoint new GLatLng
mouseClickEventType EventsList.MouseClick “click”
mouseOverEventType EventsList.MouseOver “mouseover”
onClick callback function(_e, _c) function(overlay, point)
onEndMapDraw callback getCenterLatLon
openInfoWindow openSmartWindow openInfoWindowHtml
pLat p.Lat p.y
pLon p.Lon p.x
panToPt panToLatLon panTo
zoomAndCenter drawZoomAndCenter getLatLng, panTo

For our application, it is convenient to set default zoom level values in our interface adapter zoomAndCenter. This enables a common calling signature using a minimum number of arguments. It also fixes a problematic default zoom level in the Google Maps API. For example, our zoomAndCenter interface adapter can be called with 1 argument (the address to be resolved) from either a Yahoo! or a Google map application. In the case of the Yahoo! map application, we default a missing zoom level parameter to level 5 (city scale); for Google, we default the parameter to 13 (their zoom level for city). In contrast, a missing zoom level parameter to the underlying Maps API is defaulted to 5 for Yahoo! but is defaulted to 0 for Google. Of course, this native Google default is not very useful.

The interface adapter section of myMap for each Maps API is listed below, first for Google and then for Yahoo!.

Listing 8: Interface Adapters for Google Maps API

...
      /* Begin Google Interface Adapters */
      var allocMap = function(el) { return new GMap2(el); }
      var applyUIControls = function() {
        map.setUIToDefault();
        map.addControl(new GOverviewMapControl());
      }
      // Google provides an interface to query whether the current browser is supported.
      // A similar (but possibly unreliable) test can be simulated using the Yahoo! API.
      // See below.
      var browserIsCompatible = function() {
          return GBrowserIsCompatible();
      }
      var captureEvent = function(_obj, _event, _action) { GEvent.addListener(_obj, _event, _action); }
      var createMarker = function(p) { return new GMarker(p); }
      var eraseOverlays = function() { map.clearOverlays(); }
      var getZoomLevel = function() { map.getZoom(); }
      var makePointFromFields = function() { return new GLatLng($EL("lat").value, $EL("lon").value); }
      var mouseClickEventType = "click";
      var mouseOverEventType = "mouseover";
      var onClick = function(overlay, point) { 
        if (point) { 
          panAndMark(point);
          showPoint(point);
        }
      }
      var openInfoWindow = function(mrkr, html) { mrkr.openInfoWindowHtml(html); }
      var pLat = function(p) { return p.y; }
      var pLon = function(p) { return p.x; }
      var panToPt = function(p) { map.panTo(p); }
      // In Google, zooming and centering using address lookup must be implemented
      // with an asynchronous callback attached to the .getLatLng method.
      // While in Yahoo, the callback occurs when the endMapDraw event is fired.
     var zoomAndCenter = function(address, z) {
        (new GClientGeocoder).getLatLng(
          address,
          function(point) {
            if (!point) {
              alert(address + " not found");
            } else {
              map.setCenter(point, z || 13); // default to city view
              panAndMark(point, DONT_PAN)
              showPoint(point);
              // alert("Current Google zoom level: "+getZoomLevel());
            }
          }
        );
      }
      /* End Google Interface Adapters */
...

Listing 9: Interface Adapters for Yahoo! Maps API

...
      /* Begin Yahoo! Interface Adapters */
      var runOnceOnEndMapDraw = 1;
      var allocMap = function(el) { return new YMap(el); }
      var applyUIControls = function() {
        map.addTypeControl();
        map.addZoomLong();
        map.addPanControl();
      }
      // Google provides an interface to query whether the current browser is supported.
      // A similar (but possibly unreliable) test can be simulated using the Yahoo! API.
      var browserIsCompatible = function() {
          return YMAPPID;
      }
      var captureEvent = function(_obj, _event, _action) { YEvent.Capture(_obj, _event, _action); }
      var createMarker = function(p) { return new YMarker(p); }
      var createPoint = function(lat, lon) { return new YGeoPoint(lat, lon); }
      var endMapDrawEventType = EventsList.endMapDraw; 
      var eraseOverlays = function() { map.removeMarkersAll(); }
      // var getZoomLevel = function() { map.getZoomLevel(); }
      var makePointFromFields = function() { return new YGeoPoint($EL("lat").value, $EL("lon").value); }
      var mouseClickEventType = EventsList.MouseClick;
      var mouseOverEventType = EventsList.MouseOver;
      var onClick = function(_e, _c) {
        var point = createPoint( _c.Lat, _c.Lon );
        panAndMark(point);
        showPoint(point);
      }
      // In Yahoo!, the endMapDraw event is fired after address lookup
      // and map centering have completed.
      var onEndMapDraw = function() {
        if (runOnceOnEndMapDraw > 0) {
          runOnceOnEndMapDraw = 0;
          var point = map.getCenterLatLon(); 
          panAndMark(point, DONT_PAN)
          showPoint(point); 
        }
      }
      var openInfoWindow = function(mrkr, html) { mrkr.openSmartWindow(html); }
      var pLat = function(p) { return p.Lat; }
      var pLon = function(p) { return p.Lon; }
      var panToPt = function(p) { map.panToLatLon(p); }
      var zoomAndCenter = function(address, z) {
          map.drawZoomAndCenter(address, z || 5); // default to city view 
          // alert("Current Yahoo zoom level: "+getZoomLevel());
      }
      /* End Yahoo! Interface Adapters */
...

Private Application Functions

The next section of our application implements more private functions that we use internally. With minor exceptions, these are the same for both the Yahoo! and Google maps APIs. The previously defined wrapper functions help maintain this close similarity.

Let’s look at each private function in turn.

showPoint

showPoint sets the values of the fields for Lattitude and Longitude on the HTML page (see Figure 1). Note that the attribute names within a point object are different between Yahoo! and Google. Therefore we use the wrapping functions pLat and pLon to preserve the same high level interface across Yahoo! and Google.

Listing 10: showPoint private function

...
      var showPoint = function(p) {
        $EL("lat").value = pLat(p);
        $EL("lon").value = pLon(p);
      }
...

setFields

setFields sets the visible input fields for Lattitude and Longitude to new values.

Listing 11: setFields private function

...
      var setFields = function(lat, lon) {
        $EL("lat").value = lat;
        $EL("lon").value = lon;
      }
...

panAndMark

panAndMark clears the map of any previous markers and subsequently positions the map to the point indicated by the first parameter p. Then a new marker is created at that point. If this marker is near the bounding box of one of the target features (e.g., Golden Gate Bridge, Hallepoort or Ile de la Cite), an information window is attached to the marker, using our wrapper function openInfoWindow, and will be displayed when the marker gets a MouseOver event. The second argument to panAndMark, dont_pan, controls whether to pan to the given point during the function execution. Avoiding a repeat pan operation is useful when the underlying API has already centered on the desired point.

Listing 12: panAndMark private function

...
      var panAndMark = function(p, dont_pan) {
        eraseOverlays();
        if (!dont_pan) panToPt(p);
        var marker = createMarker(p);
        var html = targetFeatures.isNearby(p);
        if (html) {
          captureEvent(marker, mouseOverEventType, function() {
            openInfoWindow(marker, html);
          });
        }
        map.addOverlay(marker);
      }
...

setTargetFeatures

The next private function is a bit more complex. When executed, the function literal which is assigned to setTargetFeatures creates a closure for its argument targets and exports a function literal .isNearby to its caller. Because of closure context, the exported function .isNearby continues to have access to targets when invoked later to test the proximity between a marker and a target. In the case of our example, the target feature list is contained in the global literal array defaultGeoTargets. The feature list, however, could just as easily have been obtained from an alternative source like JSON. Here, the advantage of applying closure to the argument targets is that the function does not need to know anything about, and is thereby decoupled from, the structure of the caller and the global environment.

When called, the exported function .isNearby simply iterates through the target list and checks to see if the given point p is contained within the bounding box of each target in turn. If so, the HTML string associated with that target is returned to the caller. In our example, this HTML will be rendered in the marker pop up information window when the marker is activated by a MouseOver event.

As a side effect, when the function literal is called from .initMap, it also sets the value of the address field in the application’s HTML body to the first element of the first target. This becomes the address which will be rendered as the initial location of the map.

Listing 13: setTargetFeatures private function

...
      var setTargetFeatures = function(targets) {
        if (targets) {
          $EL("address").value = targets[0].address; // first element of first target is default address
        }
        return {
          isNearby: function(p) {
            var found = null;
            for (var i=0; i < targets.length; i++) {
              b = targets[i].geobox;
              if (pLon(p) < b.E && pLon(p) > b.W && pLat(p) < b.N && pLat(p) > b.S) {
                var target = targets[i];
                found = targets[i].html;
                break;
              }
            }
            return found;
          }
        }
      }
...

Public Application Functions

The next section of our application embodies the public interface of our application. When the JavaScript interpreter reads the function literal on the right hand side of the assignment statement for myMap, it executes the function literal and assigns the returned object to myMap. The returned object is an associative array containing the public functions of our maps application.

Listing 14: Hash of public functions (template)

...
      return {
        //
        // public functions expressed as a literal hash object
        //
        initMap: ...
        showMap: ...
        reMap: ...
        remoteMap: ...
        changeAddress: ...
      }
...

The public functions are similar across both APIs. Each public function is described in the following sections.

initMap Public Function

The public function .initMap first checks to see if the current browser is compatible with the selected Maps API. In the Google Maps API, this function is explicitly supported. In the Yahoo package, there does not appear to be a way to programmatically tell if the current browser is supported, so we have created a stub which simply echos the Yahoo! Map ID that was provided in the request to download the JavaScript maps library.

If the browser is found to be compatible, the targetFeatures object is created which defines a method to determine if a marker on the map lies within the bounding box of one of our target features. This check is invoked whenever a MouseOver event is raised for a marker on the map.

Then the following steps are executed: (1) the base map object is allocated by the selected Maps API, (2) listeners are set up for map events, and (3) a selection of standard UI controls (map navigation and zoom elements) are applied. Notice that an additional map event is needed for the Yahoo interface to capture the completion of map drawing which then allows placement of an initial marker (see below for more details).

Finally, the map along with an initial location and marker is displayed using a call to our own public .showMap function. Because .showMap must be invoked on an object, we use the current object reference this as the receiving object.

The Google based initMap function is:

Listing 15: initMap public function for Google Maps API

...
        initMap: function() {
          if (!browserIsCompatible()) return;
          targetFeatures = setTargetFeatures(defaultGeoTargets);
          map = allocMap($EL('map_canvas'));
          captureEvent(map, mouseClickEventType, onClick);
          applyUIControls();
          this.showMap();
        },
...

The Yahoo! based initMap function is:

Listing 16: initMap public function for Yahoo! Maps API

...
        initMap: function() {
          if (!browserIsCompatible()) return;
          targetFeatures = setTargetFeatures(defaultGeoTargets);
          map = allocMap($EL('map_canvas'));
          captureEvent(map, mouseClickEventType, onClick);
--->      captureEvent(map, endMapDrawEventType, onEndMapDraw); <--- additional event
          applyUIControls();
          this.showMap();
        },
...

showMap Public Function

.showMap performs the address resolution, centering and zooming procedures required to display a map for an initial or a new address. .showMap is exported as a public function so that it can be invoked by the .changeAddress function of the maps application running in the other iframe. In this way the two iframes can be kept in sync if allowed by the sync checkbox. Note that our Yahoo version must set a recursion limiting switch, the runOnceOnEndMapDraw flag, in order to allow the endMapDraw callback to perform centering and marker placement.

Notice that for the Yahoo! API, an additional event, endMapDraw, must be used to draw an initial marker on the map whenever a new address is resolved. This is because, in the Yahoo! API, the center of the map can only be retrieved after the map has finished being drawn. This is an inherently asynchronous process. The Yahoo! API uses drawZoomAndCenter to resolve, center and zoom on an address. Then the callback registered for the endMapDraw event can retrieve the new map’s center and place the initial marker. The Google API uses the geocoding function .getLatLng to first resolve an address and then invoke a callback, given as a parameter to .getLatLng, to center the map and place an initial marker. These procedures are executed in the interface function zoomAndCenter which was shown earlier in the section Private Interface Adapters.

The Yahoo! steps involved in zoomAndCenter (with help from .initMap, onEndMapDraw and .showMap) are the following:

  • .drawZoomAndCenter: (1) resolve, (2) center and (3) zoom on address
  • endMapDraw callback: (4) get center & (5) place marker

The Google steps in zoomAndCenter are the following:

  • .getLatLng: (1) resolve address
  • getLatLng callback: (2) set center, (3) zoom and (4) mark

The Google based showMap function is:

Listing 17: showMap public function for Google Maps API

...
        showMap: function(address) {
          if (!map) return;
          $EL("address").value = address || $EL('address').value;
          zoomAndCenter($EL("address").value);
        },
...

The Yahoo! based showMap function is:

Listing 18: showMap public function for Yahoo! Maps API

...
        showMap: function(address) {
          if (!map) return;
          $EL("address").value = address || $EL('address').value;
--->      runOnceOnEndMapDraw = 1;
          zoomAndCenter($EL("address").value);  
        },
...

Notice the runOnceOnEndMapDraw flag in the Yahoo function. As already mentioned above, runOnceOnEndMapDraw is used to limit the depth of recursion in the callback attached to the endMapDraw event.

reMap Public Function

.reMap is invoked when the user presses the Set button (see Figure 1). Typically the user has changed the values of the latitude and longitude input fields in order to recenter the map. .reMap performs the following steps: (1) sets the value of the address input text field to the string “CUSTOM”, (2) creates a point object (for the appropriate Maps API using our wrapper function), (3) pans to the coordinates of this point object, and (4) places a marker at the location. If the sync checkbox has been selected, the maps application running in the other iframe will be requested to pan to, mark and display the same coordinates.

The Google based reMap function is:

Listing 19: reMap public function for Google Maps API

...
        reMap: function() {
          if (!map) return;
          $EL("address").value = "CUSTOM";
          point = makePointFromFields();
          panAndMark(point);
          showPoint(point);
          if ($EL('sync').checked) parent.yahoo.myMap.remoteRemap(pLat(point), pLon(point));
        },
...

And the Yahoo based reMap function is:

Listing 20: reMap public function for Yahoo! Maps API

...
        reMap: function() {
          if (!map) return;
          $EL("address").value = "CUSTOM";
          point = makePointFromFields();
          panAndMark(point);
          showPoint(point);
          if ($EL('sync').checked) parent.google.myMap.remoteRemap(pLat(point), pLon(point));
        },
...

remoteMap Public Function

remoteMap is invoked by the maps application running in the other iframe when the user presses the Set button of that iframe to re-center the map using the current values of the latitude and longitude fields. It is invoked from the .reMap function of the other iframe. The sync checkbox of the other iframe must be selected for the cross-frame call to be made.

The Google and Yahoo based remoteMap functions are identical:

Listing 21: remoteMap public function for Google & Yahoo! Maps API

...
        remoteRemap: function(lat, lon) {
          if (!map) return;
          $EL("address").value = "CUSTOM";
          setFields(lat, lon);
          point = makePointFromFields();
          panAndMark(point);
          showPoint(point);
        },
...

changeAddress Public Function

changeAddress is invoked when the user manually changes the value of the address input field in order to resolve a new address and to recenter the map. If the sync checkbox is selected, then the .showMap function of the other iframe is also invoked in order to synchronize the two maps.

The Google based changeAddress function is:

Listing 22: changeAddress public function for Google Maps API

...
        changeAddress: function (addr) {
          if (!map) return;
          this.showMap(addr);
          if ($EL('sync').checked) parent.yahoo.myMap.showMap(addr);
        }
...

The Yahoo! based changeAddress function is:

Listing 23: changeAddress public function for Yahoo! Maps API

...
        changeAddress: function (addr) {
          if (!map) return;
          this.showMap(addr);
          if ($EL('sync').checked) parent.google.myMap.showMap(addr);
        }
...

Finishing Up With The HTML Body

Our main JavaScript application is invoked when the onLoad event for the body tag is fired. That is, when all of the resources used by the HTML page have been loaded. When using the Google Maps API, it is recommended that the onUnload attribute be set to trigger a call to GUnload when leaving the page in order to avoid memory leaks.

Here is the Google version:

Listing 24: onLoad and onUnload attributes in body tag for Google Maps API

...
  </head>
  <body onload="myMap.initMap();" onunload="GUnload();"> 
...

Here is the Yahoo! version:

Listing 25: onLoad attribute in body tag for Yahoo! Maps API

...
  </head>
  <body onload="myMap.initMap();"> 
...

The header for the HTML page within each iframe is adjusted for each Maps API:

Here’s the Google header:

Listing 26: Page header for Google map

...
    <h2><font style="color: white; background: #ccc;">Google</font> Maps API</h2>
    <div style="width: 400px;">
...

And the Yahoo! header:

Listing 27: Page header for Yahoo map

...
    <h2><font style="color: white; background: #ccc;">Yahoo!</font> Maps API</h2>
    <div style="width: 400px;"> 
...

The address input field allows the user to change the current address rendered by the map.

Listing 28: Address input field

...
      <p>Address: <input size=40 type="text" id="address" value=""
                            onChange="myMap.changeAddress(this.value);"></p>
...

The sync checkbox instructs the application to send any address changes to the sibling iframe containing the alternative Maps API.

Here’s the Google version:

Listing 29: Sync checkbox for Google iframe

...
      <p><input type="checkbox" id="sync"> Keep address synced with Yahoo! Map</p>
...

Here’s the Yahoo version:

Listing 30: Sync checkbox for Yahoo iframe

...
      <p>Keep address synced with Google Map <input type="checkbox" id="sync"></p> 
...

The following section builds a dynamic list of target features, taken from global variable defaultGeoTargets. These are links that can be selected by the user (see Figure 1).

Listing 31: Dynamic creation of map selection links

...
      <p>Mouseover the marker near
<ul>
<script>
  for (var i=0; i < defaultGeoTargets.length; i++) {
    document.writeln("<li><a href='javascript:myMap.changeAddress(defaultGeoTargets["+i+"].address)'>"+defaultGeoTargets[i].address+"</a></li>")
  }
</script>
</ul>
to show its picture. Click on map for other coordinates.</p>
    </div>
...

map_canvas is the HTML element that acts as a container for the map rendered by the selected Maps API. The Maps API uses the explicit dimensions of this container to size the map. In addition, style attributes may be inherited from this container by pop up information windows attached to markers. Inherited styles like font-size can conflict with layout calculations made by the Maps API for the pop up windows since these calculations are made independently of any styles actually specified for the container. See Mike Williams' Fixing the “inherited CSS” problem for more details.

Listing 32: map_canvas map container element

...
    <div id="map_canvas" style="width: 400px; height: 300px"></div>
    <br />
...

The user can adjust the latitude and longitude of the center of the map by entering in new values in the respective fields below and then pressing the Set button. Synchronization with the Maps API of the other iframe is maintained if the sync checkbox is checked.

Listing 33: Longitude, latitude input fields

...
    <table>
      <tr><td align="left"><a href="http://en.wikipedia.org/wiki/Latitude">Latitude</a>:</td><td><input size="18" type="text" id="lat" value="" ></td></tr>
      <tr><td align="left"><a href="http://en.wikipedia.org/wiki/Longitude">Longitude</a>:</td><td><input size="18" type="text" id="lon" value="" ></td></tr>
      <tr><td align="left" colspan="2"><input type="submit" value="Set" onClick="myMap.reMap($EL('address'));"></td></tr>
    </table>
  </body>
</html>
...

Complete Listings

The following listings present the complete code for each iframe component. The Yahoo code is shown first, followed by the Google code.

Yahoo Iframe Page

(Full Listing)

Listing 34: Yahoo Maps API application

<!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>
    <script src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=[YOUR YAHOO MAPS API KEY]"></script>  
    <script>
    var defaultGeoTargets = [
      {
        address: 'Golden Gate Bridge, San Francisco',
        geobox: { N:37.833, E:-122.474, S:37.805, W:-122.482 },
        html: "<a href='http://en.wikipedia.org/wiki/Golden_Gate_Bridge' target='_blank'><img src='/images/sf_bridge_small.jpg' width='160px' height='120px'></a>"
      },
      {
        address: 'Hallepoort, Brussels',
        geobox: { N:50.834, E:4.345, S:50.833, W:4.344 },
        html: "<p style='width:200px'><a href='http://en.wikipedia.org/wiki/Halle_Gate' target='_blank'><img src='/mapsapis/images/hallepoort_small.jpg' width='120px' height='160px'></a><span style='position: absolute;'><p>&nbsp;Hallepoort<br />&nbsp;today.</p></span></p>"
      },
      {
        address: 'Ile de la Cite, Paris',
        geobox: { N:48.86, E:2.349, S:48.85, W:2.341 },
        html: "<a href='http://en.wikipedia.org/wiki/Ile_de_la_Cite' target='_blank'><img src='/images/ile_de_la_cite.jpg' width='200px' height='150px'></a>"
      }
    ];
    var $EL = function(id) { return document.getElementById(id); };
    var myMap = function() {
      var map;
      var targetFeatures;
      var DONT_PAN = true;
      /* Begin Yahoo! Interface Adapters */
      var runOnceOnEndMapDraw = 1;
      var allocMap = function(el) { return new YMap(el); }
      var applyUIControls = function() {
        map.addTypeControl();
        map.addZoomLong();
        map.addPanControl();
      }
      var browserIsCompatible = function() {
          return YMAPPID;
      }
      var captureEvent = function(_obj, _event, _action) { YEvent.Capture(_obj, _event, _action); }
      var createMarker = function(p) { return new YMarker(p); }
      var createPoint = function(lat, lon) { return new YGeoPoint(lat, lon); }
      var endMapDrawEventType = EventsList.endMapDraw; 
      var eraseOverlays = function() { map.removeMarkersAll(); }
      // var getZoomLevel = function() { map.getZoomLevel(); }
      var makePointFromFields = function() { return new YGeoPoint($EL("lat").value, $EL("lon").value); }
      var mouseClickEventType = EventsList.MouseClick;
      var mouseOverEventType = EventsList.MouseOver;
      var onClick = function(_e, _c) {
        var point = createPoint( _c.Lat, _c.Lon );
        panAndMark(point);
        showPoint(point);
      }
      var onEndMapDraw = function() {
        if (runOnceOnEndMapDraw > 0) {
          runOnceOnEndMapDraw = 0;
          var point = map.getCenterLatLon(); 
          panAndMark(point, DONT_PAN)
          showPoint(point); 
        }
      }
      var openInfoWindow = function(mrkr, html) { mrkr.openSmartWindow(html); }
      var pLat = function(p) { return p.Lat; }
      var pLon = function(p) { return p.Lon; }
      var panToPt = function(p) { map.panToLatLon(p); }
      var zoomAndCenter = function(address, z) {
          map.drawZoomAndCenter(address, z || 5); // default to city view 
          // alert("Current Yahoo zoom level: "+getZoomLevel());
      }
      /* End Yahoo! Interface Adapters */
      var showPoint = function(p) {
        $EL("lat").value = pLat(p);
        $EL("lon").value = pLon(p);
      }
      var setFields = function(lat, lon) {
        $EL("lat").value = lat;
        $EL("lon").value = lon;
      }
      var panAndMark = function(p, dont_pan) {
        eraseOverlays();
        if (!dont_pan) panToPt(p);
        var marker = createMarker(p);
        var html = targetFeatures.isNearby(p);
        if (html) {
          captureEvent(marker, mouseOverEventType, function() {
            openInfoWindow(marker, html);
          });
        }
        map.addOverlay(marker);
      }
      var setTargetFeatures = function(targets) {
        if (targets) {
          $EL("address").value = targets[0].address; // first element of first target is default address
        }
        return {
          isNearby: function(p) {
            var found = null;
            for (var i=0; i < targets.length; i++) {
              b = targets[i].geobox;
              if (pLon(p) < b.E && pLon(p) > b.W && pLat(p) < b.N && pLat(p) > b.S) {
                var target = targets[i];
                found = targets[i].html;
                break;
              }
            }
            return found;
          }
        }
      }
      return {
        initMap: function() {
          if (!browserIsCompatible()) return;
          targetFeatures = setTargetFeatures(defaultGeoTargets);
          map = allocMap($EL('map_canvas'));
          captureEvent(map, mouseClickEventType, onClick);
          captureEvent(map, endMapDrawEventType, onEndMapDraw);
          applyUIControls();
          this.showMap();
        },
        showMap: function(address) {
          if (!map) return;
          $EL("address").value = address || $EL('address').value;
          runOnceOnEndMapDraw = 1;
          zoomAndCenter($EL("address").value);  
        },
        reMap: function() {
          if (!map) return;
          $EL("address").value = "CUSTOM";
          point = makePointFromFields();
          panAndMark(point);
          showPoint(point);
          if ($EL('sync').checked) parent.google.myMap.remoteRemap(pLat(point), pLon(point));
        },
        remoteRemap: function(lat, lon) {
          if (!map) return;
          $EL("address").value = "CUSTOM";
          setFields(lat, lon);
          point = makePointFromFields();
          panAndMark(point);
          showPoint(point);
        },
        changeAddress: function (addr) {
          if (!map) return;
          this.showMap(addr);
          if ($EL('sync').checked) parent.google.myMap.showMap(addr);
        }
      }
    }();
    </script>
  </head>
  <body onload="myMap.initMap();"> 
    <h2><font style="color: white; background: #ccc;">Yahoo!</font> Maps API</h2>
    <div style="width: 400px;">
      <p>Address: <input size=40 type="text" id="address" value="" onChange="myMap.changeAddress(this.value);"></p>
      <p>Keep address synced with Google Map <input type="checkbox" id="sync"></p>
      <p>Mouseover the marker near
<ul>
<script>
  for (var i=0; i < defaultGeoTargets.length; i++) {
    document.writeln("<li><a href='javascript:myMap.changeAddress(defaultGeoTargets["+i+"].address)'>"+defaultGeoTargets[i].address+"</a></li>")
  }
</script>
</ul>
to show its picture. Click on map for other coordinates.</p>
    </div>
    <div id="map_canvas" style="width: 400px; height: 300px"></div>
    <br />
    <table>
      <tr><td align="left"><a href="http://en.wikipedia.org/wiki/Latitude">Latitude</a>:</td><td><input size="18" type="text" id="lat" value="" ></td></tr>
      <tr><td align="left"><a href="http://en.wikipedia.org/wiki/Longitude">Longitude</a>:</td><td><input size="18" type="text" id="lon" value="" ></td></tr>
      <tr><td align="left" colspan="2"><input type="submit" value="Set" onClick="myMap.reMap();"></td></tr>
    </table>
  </body>
</html>

Google Iframe Page

(Full Listing)

Listing 35: Google Maps API application

<!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>
    <script src="http://maps.google.com/maps?file=api&v=2&key=[YOUR GOOGLE MAPS API KEY]&sensor=false"></script>
    <script>
    var defaultGeoTargets = [
      {
        address: 'Golden Gate Bridge, San Francisco',
        geobox: { N:37.833, E:-122.474, S:37.805, W:-122.482 },
        html: "<a href='http://en.wikipedia.org/wiki/Golden_Gate_Bridge' target='_blank'><img src='/images/sf_bridge_small.jpg' width='160px' height='120px'></a>"
      },
      {
        address: 'Hallepoort, Brussels',
        geobox: { N:50.834, E:4.345, S:50.833, W:4.344 },
        html: "<a href='http://en.wikipedia.org/wiki/Halle_Gate' target='_blank'><img src='/images/hallepoort_1612.jpg' width='120px' height='160px'></a><span style='position: absolute;'><p>&nbsp;Hallepoort<br />&nbsp;in 1612.</p></span>"
      },
      {
        address: 'Ile de la Cite, Paris',
        geobox: { N:48.86, E:2.349, S:48.85, W:2.341 },
        html: "<a href='http://en.wikipedia.org/wiki/Ile_de_la_Cite' target='_blank'><img src='/images/ile_de_la_cite.jpg' width='200px' height='150px'></a>"
      }
    ];
    var $EL = function(id) { return document.getElementById(id); };
    var myMap = function() {
      var map;
      var targetFeatures;
      var DONT_PAN = true;
      /* Begin Google Interface Adapters */
      var allocMap = function(el) { return new GMap2(el); }
      var applyUIControls = function() {
        map.setUIToDefault();
        map.addControl(new GOverviewMapControl());
      }
      var browserIsCompatible = function() {
          return GBrowserIsCompatible();
      }
      var captureEvent = function(_obj, _event, _action) { GEvent.addListener(_obj, _event, _action); }
      var createMarker = function(p) { return new GMarker(p); }
      var eraseOverlays = function() { map.clearOverlays(); }
      var getZoomLevel = function() { map.getZoom(); }
      var makePointFromFields = function() { return new GLatLng($EL("lat").value, $EL("lon").value); }
      var mouseClickEventType = "click";
      var mouseOverEventType = "mouseover";
      var onClick = function(overlay, point) { 
        if (point) { 
          panAndMark(point);
          showPoint(point);
        }
      }
      var openInfoWindow = function(mrkr, html) { mrkr.openInfoWindowHtml(html); }
      var pLat = function(p) { return p.y; }
      var pLon = function(p) { return p.x; }
      var panToPt = function(p) { map.panTo(p); }
      var zoomAndCenter = function(address, z) {
        (new GClientGeocoder).getLatLng(
          address,
          function(point) {
            if (!point) {
              alert(address + " not found");
            } else {
              map.setCenter(point, z || 13); // default to city view
              panAndMark(point, DONT_PAN)
              showPoint(point);
              // alert("Current Google zoom level: "+getZoomLevel());
            }
          }
        );
      }
      /* End Google Interface Adapters */
      var showPoint = function(p) {
        $EL("lat").value = pLat(p);
        $EL("lon").value = pLon(p);
      }
      var setFields = function(lat, lon) {
        $EL("lat").value = lat;
        $EL("lon").value = lon;
      }
      var panAndMark = function(p, dont_pan) {
        eraseOverlays();
        if (!dont_pan) panToPt(p);
        var marker = createMarker(p);
        var html = targetFeatures.isNearby(p);
        if (html) {
          captureEvent(marker, mouseOverEventType, function() {
            openInfoWindow(marker, html);
          });
        }
        map.addOverlay(marker);
      }
      var setTargetFeatures = function(targets) {
        if (targets) {
          $EL("address").value = targets[0].address; // first element of first target is default address
        }
        return {
          isNearby: function(p) {
            var found = null;
            for (var i=0; i < targets.length; i++) {
              b = targets[i].geobox;
              if (pLon(p) < b.E && pLon(p) > b.W && pLat(p) < b.N && pLat(p) > b.S) {
                var target = targets[i];
                found = targets[i].html;
                break;
              }
            }
            return found;
          }
        }
      }
      return {
        initMap: function() {
          if (!browserIsCompatible()) return;
          targetFeatures = setTargetFeatures(defaultGeoTargets);
          map = allocMap($EL('map_canvas'));
          captureEvent(map, mouseClickEventType, onClick);
          applyUIControls();
          this.showMap();
        },
        showMap: function(address) {
          if (!map) return;
          $EL("address").value = address || $EL('address').value;
          zoomAndCenter($EL("address").value);
        },
        reMap: function() {
          if (!map) return;
          $EL("address").value = "CUSTOM";
          point = makePointFromFields();
          panAndMark(point);
          showPoint(point);
          if ($EL('sync').checked) parent.yahoo.myMap.remoteRemap(pLat(point), pLon(point));
        },
        remoteRemap: function(lat, lon) {
          if (!map) return;
          $EL("address").value = "CUSTOM";
          setFields(lat, lon);
          point = makePointFromFields();
          panAndMark(point);
          showPoint(point);
        },
        changeAddress: function (addr) {
          if (!map) return;
          this.showMap(addr);
          if ($EL('sync').checked) parent.yahoo.myMap.showMap(addr);
        }
      }
    }();
    </script>
  </head>
  <body onload="myMap.initMap();" onunload="GUnload();"> 
    <h2><font style="color: white; background: #ccc;">Google</font> Maps API</h2>
    <div style="width: 400px;">
      <p>Address: <input size=40 type="text" id="address" value="" onChange="myMap.changeAddress(this.value);"></p>
      <p><input type="checkbox" id="sync"> Keep address synced with Yahoo! Map</p>
      <p>Mouseover the marker near
<ul>
<script>
  for (var i=0; i < defaultGeoTargets.length; i++) {
    document.writeln("<li><a href='javascript:myMap.changeAddress(defaultGeoTargets["+i+"].address)'>"+defaultGeoTargets[i].address+"</a></li>")
  }
</script>
</ul>
to show its picture. Click on map for other coordinates.</p>
    </div>
    <div id="map_canvas" style="width: 400px; height: 300px"></div>
    <br />
    <table>
      <tr><td align="left"><a href="http://en.wikipedia.org/wiki/Latitude">Latitude</a>:</td><td><input size="18" type="text" id="lat" value="" ></td></tr>
      <tr><td align="left"><a href="http://en.wikipedia.org/wiki/Longitude">Longitude</a>:</td><td><input size="18" type="text" id="lon" value="" ></td></tr>
      <tr><td align="left" colspan="2"><input type="submit" value="Set" onClick="myMap.reMap($EL('address'));"></td></tr>
    </table>
  </body>
</html>

© 2000-2010 Technetra. All rights reserved. Contact | Terms of Use

WordPress