http://www.developer.com/

Back to article

Building WML Gadgets: A Calendar


December 20, 2002

This series of articles describes how to provide Web content to mobile devices through WML (Wireless Markup Language). This article covers creating small programs that extend the functionality of mobile devices, building on the previous two articles and examples:

Note: These articles cover WML and WMLScript version 1.1, which are supported by the majority of mobile devices in use today. The articles assume a working knowledge of HTML and general Web technologies, and further assume that you have read the previous article(s) in this series.

Review

As mentioned in the last few articles, it is possible to add value to a mobile device by creating a small but ultimately useful application. In designing such an application, remember that the user will need to be online to use it, so the application's utility needs to be weighed against the potential cost of use.

Note: Most mobile service plans offer a base amount of online time dedicated to "Web" use. So the user generally isn't paying more for the occasional gadget use.

Simple Calendar

To round out our small suite of useful gadgets, let's add a simple calendar application. This application displays a monthly calendar and enables the user to move forward and backward through the months.

A Little Help from CGI

We could accomplish this gadget using only WML and WMLS. However, it would require writing some pretty extensive code to do date mathematics in order to correctly display any month within any year. Instead, we will utilize a CGI program written in Perl.

Why Perl? We could accomplish the same results with almost any Web programming language that supports date libraries (such as PHP), but Perl offers a few advantages:

  • Perl is highly extensible.
    A variety of modules are available to extend Perl and give us the functionality we need.

  • Perl is platform-independent.
    Perl is available for most platforms and is easily implemented on each.

  • Perl hasn't been used in this series of articles.
    If you would rather use another technology, such as PHP, simply apply the concepts outlined here to your preferred technology, using previous WML and PHP (or your preferred technology) articles for help where necessary.

Setting the Stage

As with other projects, we have to be careful how we design our application, given the small amount of screen real estate available. I started this project by using the output of the Unix "cal" program. This program's base functionality provides small monthly calendars such as this:

   December 2002
Su Mo Tu We Th Fr Sa
 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

This format fits nicely in the screen of a small device such as a cell phone. In fact, it fits so nicely that my first attempt at this gadget used the output straight from the cal program.

Using a filter, I was able to add line breaks ("<br/>") to the end of each line. Adding an appropriate WML shell around the resulting output gave me the following code:

<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
  "http://www.wapforum.org/DTD/wml_1.1.xml">

<wml>
<card>
<p>
   December 2002<br/>
Su Mo Tu We Th Fr Sa<br/>
 1  2  3  4  5  6  7<br/>
 8  9 10 11 12 13 14<br/>
15 16 17 18 19 20 21<br/>
22 23 24 25 26 27 28<br/>
29 30 31<br/>
</p>
</card>
</wml>

However, the output was something of a train wreck, as shown in the following figure:

FIGURE 11.1

All Image in this article are courtesy Openwave Systems Inc. (Openwave, the Openwave logo, Openwave SDK, Openwave SDK Universal Edition, Openwave SDK WAP Edition are trademarks of Openwave Systems Inc. All rights reserved.)

The problem lies in the mobile browser's font; because it uses a proportional font, the columns of numbers don't line up correctly. In fact, I quickly determined that WML wouldn't allow me to use the pure text calendar—it lacks even rudimentary font control outside of the usual bold, italic, and underline. The equivalent of a <PRE> tag would be very welcome.

Refining the Output

There is one WML construct that would aid our format: tables. Using a seven-column design, we could put each date in a cell and rely on the table format to align the dates properly. Using this method, our calendar display code becomes the following:

<p>
December 2002<br/>
<table columns="7">
<tr><td>Su</td><td>Mo</td><td>Tu</td><td>We</td>
<td>Th</td><td>Fr</td><td>Sa</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td>
<td>5</td><td>6</td><td>7</td></tr>
<tr><td>8</td><td>9</td><td>10</td><td>11</td>
<td>12</td><td>13</td><td>14</td></tr>
<tr><td>15</td><td>16</td><td>17</td><td>18</td>
<td>19</td><td>20</td><td>21</td></tr>
<tr><td>22</td><td>23</td><td>24</td><td>25</td>
<td>26</td><td>27</td><td>28</td></tr>
<tr><td>29</td><td>30</td><td>31</td></tr>
</table>
</p>

The resulting output resembles this figure:

FIGURE 11.2

This is much closer to the desired effect, but could use a little more polish. Adding leading spaces to the days under 10 and some separating bars gets us about as close as possible:


<p>
December 2002<br/>
<table columns="7">
<tr><td>Su</td><td>Mo</td><td>Tu</td><td>We</td>
<td>Th</td><td>Fr</td><td>Sa</td></tr>
<tr><td>&nbsp;1|</td><td>&nbsp;2|</td>
<td>&nbsp;3|</td><td>&nbsp;4|</td><td>&nbsp;5|</td>
<td>&nbsp;6|</td><td>&nbsp;7|</td></tr>
<tr><td>&nbsp;8|</td><td>&nbsp;9|</td><td>10|</td>
<td>11|</td><td>12|</td><td>13|</td><td>14|</td></tr>
<tr><td>15|</td><td>16|</td><td>17|</td><td>18|</td>
<td>19|</td><td>20|</td><td>21|</td></tr>
<tr><td>22|</td><td>23|</td><td>24|</td><td>25|</td>
<td>26|</td><td>27|</td><td>28|</td></tr>
<tr><td>29|</td><td>30|</td><td>31|</td></tr>
</table>
</p>

Note: We use non-breaking space entities ("&nbsp;") instead of straight spaces because WML abhors white space—it otherwise would fail to recognize and render the white space.

The output of this new code now resembles the following figure:

FIGURE 11.3

Finding the Right Tool

As mentioned earlier, in an effort to not re-create the wheel, I started with the output from the Unix cal command. Using a filter, I was easily able to add line break tags to the output. However, using a similar filter to insert appropriate table tags would be an exercise in futility. The permutations for what and where to insert row and cell tags are too many to even count, let alone parse.

Instead, I turned to Perl and the Date::Calc module from Steffen Beyer, found at www.engelschall.com. ( http://www.engelschall.com/~sb/download/) This module contains all the date functionality needed for our simple application and will allow us to mimic the output of the cal program while including the changes to make it more legible.

Using Perl and the Date::Calc module, a simple calendar-printing application was easy to accomplish:

Listing of cal.pl

1.   #!/usr/bin/perl  -w

2.   # Define modules
3.   use Date::Calc ':all';

4.   # Get current day/month/year
5.   ($year,$month,$day) = Today([$gmt]);

6.   # Find weekday (Sun - Sat) of first of the month
7.   $firstdow = Day_of_Week($year,$month,"1");
8.   # Convert Sun (7) to 0
9.   if ("$firstdow" == "7") { $firstdow = 0; }

10.  # Month text
11.  $monthtext = Month_to_Text($month);

12.  # Print WML header and beginning tags
13.  print <<ENDHEADER;
14.  <?xml version="1.0"?>
15.  <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
16.  "http://www.wapforum.org/DTD/wml_1.1.xml">

17.  <wml>
18.  <card>
19.  <p>
20.  $monthtext $year<br/>
21.  <table columns="7">
22.  <tr><td> S </td><td> M </td><td> T </td><td> W </td>
23.  <td> T </td><td> F </td><td> S </td></tr>

24.  ENDHEADER

25.  # Start first date row
26.  print "<tr>";

27.  # Print blank cells up to first day
28.  $x = 0;
29.  while ($x < $firstdow) {
30.  print "<td>&nbsp;</td>";
31.  $x++;
32.  }

33.  # Start with first day
34.  $day = 1;

35.  # Work through month from first day through last
36.  while ($day <= Days_in_Month($year,$month)) {
37.  print "<td>";
38.  if ($day < 10) { print "&nbsp;"; }
39.  print $day."|</td>";
40.  $x++;
41.  # At end of week, close row
42.  if ($x == 7) { 
43.  print "</tr>\n";
44.  # If we aren't done, start new row
45.  if ($day != Days_in_Month($year,$month)) {
46.  print "<tr>";
47.  }
48.  $x = 0
49.  }
50.  $day++;
51.  }

52.  # Fill out last row (if applicable)
53.  #   with blank cells
54.  if ($x ne 0) {
55.  while ($x <= 6) {
56.  print "<td>&nbsp;</td>";
57.  $x++;
58.  }
59.  print "</tr>";
60.  }

61.  # Close tags, card, and WML
62.  print <<ENDFOOTER;
63.  </table>
64.  </p>
65.  </card>
66.  </wml>

67.  ENDFOOTER

Note: As with earlier articles, teaching Perl is out of the scope of this article. For a tutorial on Perl, you can go to http://wdvl.internet.com/Authoring/Languages/Perl/PerlfortheWeb/toc.html.

Lines 1-3 define where to find the Perl interpreter and include the Date::Calc module in the compilation.

Line 5 sets the variables $day, $month, and $year to the current date. Note that this is the date on the server, not the date on the device.

Lines 6-9 set the variable $firstdow to the numeric equivalent of the weekday of the first day of the month, where Sun = 0 and Sat = 6. (Note: The Day_of_Week routine returns a value of 7 for Sunday, requiring line 9 to adjust it to 0 to suit our needs.)

Line 11 sets $monthtext to the full-text representation of the month (for example, "December").

Lines 12-24 print the beginning WML header and tags up through the weekday marker table row ("S M T W T F S"). Note that I decided to use single-letter markers for the weekdays-I believe it helps better align them with the columns.

Line 26 starts the first date row and lines 27-32 print blank cells ("<td>&nbsp; </td>") for the unused weekdays up to the first of the month. For example, if the first of the month is a Wednesday, this routine would print three blank cells—one each for Sunday, Monday, and Tuesday.

Line 34 sets the $day variable back to the first day.

Lines 35-51 work through the month, day by day, from the first day to the last day. Every seventh day (marked when $x = 7), the current row is closed (lines 41-42). If we aren't done with the month ($day < last day of month), a new row is started (lines 44-47).

Lines 52-60 close out the last row, printing blank cells for each day left in the week. For example, if the last day is a Thursday, this routine would print two blank cells (Friday and Saturday). The row is then closed.

Lines 61-67 close the open tags, ending the WML output.

Testing the Rudimentary Script

This script cannot be used to output directly to a device, since it doesn't pass the correct WML header. However, using the interpreter, we can output to the console and paste the results into the Openwave SDK IDE (or other simulator). Using the command "perl cal.pl" at a Unix command shell runs the script and outputs the WML code. This code is then pasted into the Openwave IDE and passed to the simulator as shown in the figure below:


Click for larger image

FIGURE 11.4

The code is pasted into the Openwave IDE and passed to the simulator.

Now that we have a working prototype, let's test a few other dates by adding command-line arguments. We replace the following lines:

# Get current day/month/year
($year,$month,$day) = Today([$gmt]);

with a more extensive routine to parse a command line:

if (@ARGV) {
  $month = (shift @ARGV);
  $year = (shift @ARGV);
} else {
  # Get current day/month/year
  ($year,$month,$day) = Today([$gmt]);
}

This routine determines whether there are command-line parameters; if so, it puts the first parameter into the $month variable and the second into the $year variable. If there are no parameters, the routine uses the current date. Now we can test other months by passing the appropriate values to the script. For example, to test April 2001, we would use the following command line:

perl cal.pl 4 2001

Note: The first few times I tested this script it failed on assorted months—months where the first day is a Sunday, or the last day is a Saturday. Getting the logic down to handle pre-and-post filling of the week rows took a while. When testing a script such as this, it is important to test the most diverse conditions possible.

Almost at the Finish Line

To finish the script, we can add a handful of features;

  • Code to pass the proper header to the browser
  • Support for name/value pair passing via HTTP
  • A mechanism to move backward and forward by month
Header Passing

Passing the header to the browser is easily accomplished with the use of another library, CGI.pm, available from several sources on the Web. (I got my copy from L. Stein at http://stein.cshl.org/WWW/software/CGI/.) This library includes a "header" function for passing HTTP-compliant headers to browsers. By adding these two lines (in appropriate positions) we achieve our goal:

use CGI qw(:standard);
print header(-type=>'text/vnd.wap.wml');
Parameter Handling

Parameter handling is a bit more involved, requiring more parsing code for handling arguments. We need support for name/value pairs to control the script through HTTP calls, but don't want to discard the command-line parameters—they are still valuable for testing.

The following snippet handles command-line parameters as well as name/value pairs and will substitute the current date if either returns an invalid argument:

# Parse command line or name/value pairs
# Command line:
if (@ARGV) { 
  $month = (shift @ARGV);
  $year = (shift @ARGV);

} else {

  # Name/value pairs
  if ($ENV{REQUEST_METHOD} eq 'GET') {
    foreach $input (split("&",$ENV{QUERY_STRING})) {
      if ($input =~ /(.*)=(.*)/) {
        ($key,$value) = ($1, $2);
        # Add keyword/value pair to a list
        $inputs{$key} = $value;
        $month = $inputs{month};
        $year = $inputs{year};
      }
    }
  }

}

# If we don't have a valid date from params,
#   set date to today (server clock)
if (!check_date($year,$month,1)) {
  ($year,$month,$day) = Today([$gmt]);
}

Note: Since we control current access to the script, we can dictate that the parameters follow a certain format:

  • Command-line parameters are numeric month followed by numeric year
  • Name/value pairs are lowercase names ("month" and "year", respectively)
  • Name/value pairs appear in that order
  • Names are paired with numeric values.

The code above does very little input checking other than to default to the current date if something is amiss.

Next and Previous Month Navigation

Adding next and previous month navigation is fairly straightforward. We simply determine what the next/previous month and year would be and use an appropriate <GO> tag with encoded name/value pairs.

Note: We determine the month and year up front to eliminate the need to do any processing later on (moving from Dec to Jan, or from Jan back to Dec).

The function Add_Delta_Days from the Date::Calc module allows us to do date math, adding days to a known date. Adding 31 to the first day of any month will give us a date in the following month, while subtracting 1 from the first day of any month will give us a date in the prior month. Such code resembles the following:

# Set next month/year
($nextyear,$nextmonth,$nextday) =
  Add_Delta_Days($year,$month,1,31);

# Set prev month/year
($prevyear,$prevmonth,$prevday) =
  Add_Delta_Days($year,$month,1,-1);

We then use the variables when outputting the <GO> tags, similar to this:

<do type="accept" label="Back" name="Prev">
  <go href="cal.pl?month=$prevmonth&amp;year=$prevyear" />
</do>

The Finished Script

When we put it all together, the script is as follows:

Listing of cal.pl

#!/usr/bin/perl  -w

# Define modules
use CGI qw(:standard);
use Date::Calc ':all';

# Parse command line or name/value pairs
# Command line:
if (@ARGV) { 
  $month = (shift @ARGV);
  $year = (shift @ARGV);

} else {

  # Name/value pairs
  if ($ENV{REQUEST_METHOD} eq 'GET') {
    foreach $input (split("&",$ENV{QUERY_STRING})) {
      if ($input =~ /(.*)=(.*)/) {
        ($key,$value) = ($1, $2);
        # Add keyword/value pair to a list
        $inputs{$key} = $value;
        $month = $inputs{month};
        $year = $inputs{year};
      }
    }
  }

}

# If we don't have a valid date from params,
#   set date to today (server clock)
if (!check_date($year,$month,1)) {
  ($year,$month,$day) = Today([$gmt]);
}

# Find weekday (Sun - Sat) of first of the month
$firstdow = Day_of_Week($year,$month,"1");
# Convert Sun (7) to 0
if ("$firstdow" == "7") { $firstdow = 0; }

# Month text
$monthtext = Month_to_Text($month);

# Set next month/year
($nextyear,$nextmonth,$nextday) =
  Add_Delta_Days($year,$month,1,31);

# Set prev month/year
($prevyear,$prevmonth,$prevday) =
  Add_Delta_Days($year,$month,1,-1);

# Pass WML header
print header(-type=>'text/vnd.wap.wml');

# Print WML header and beginning tags
print <<ENDHEADER;
<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
  "http://www.wapforum.org/DTD/wml_1.1.xml">

<wml>
<card>
<do type="accept" label="Back" name="Prev">
  <go href="cal.pl?month=$prevmonth&amp;year=$prevyear" />
</do>
<do type="option" label="Fwd" name="Next">
  <go href="cal.pl?month=$nextmonth&amp;year=$nextyear" />
</do>
<p>
$monthtext $year<br/>
<table columns="7">
<tr><td> S </td><td> M </td><td> T </td><td> W </td>
<td> T </td><td> F </td><td> S </td></tr>

ENDHEADER

# Start first date row
print "<tr>";

# Print blank cells up to first day
$x = 0;
while ($x < $firstdow) {
  print "<td>&nbsp;</td>";
  $x++;
}

# Start with first day
$day = 1;

# Work through month from first day through last
while ($day <= Days_in_Month($year,$month)) {
  print "<td>";
  if ($day < 10) { print "&nbsp;"; }
  print $day."|</td>";
  $x++;
  # At end of week, close row
  if ($x == 7) { 
    print "</tr>\n";
    # If we aren't done, start new row
    if ($day != Days_in_Month($year,$month)) {
      print "<tr>";
    }
    $x = 0
  }
  $day++;
}

# Fill out last row (if applicable)
#   with blank cells
if ($x ne 0) {
  while ($x <= 6) {
    print "<td>&nbsp;</td>";
    $x++;
  }
  print "</tr>";
}

# Close tags, card, and WML
print <<ENDFOOTER;
</table>
</p>
</card>
</wml>

ENDFOOTER

If a mobile browser without parameters calls the script, it will display the current month along with two buttons to move forward and backward by month, respectively. Alternatively, the script can be accessed with encoded name/value pairs to specify which month to display. For example, the following URL results in the display shown in the figure:

http:///cal.pl?month=4&year=2001

FIGURE 11.5

Room for Improvement

As with previous projects, several things could be added to improve our calendar:

  • Better labels for our navigation keys-the first three letters of the month (e.g., "Dec"), for example, instead of "Back" and "Fwd"
  • Marking the current date, perhaps with underlining.
  • A navigation control to allow the user to specify a particular month to display
  • A navigation control to display the current month

What Do You Want from WML?

I'm interested in hearing what you need/want to do with WML. I'll use some of the more challenging or common ideas in upcoming articles. Send your ideas to the address below.

About the Author

Steve Schafer is the chief operating officer of Progeny Linux Systems, a Linux-based consulting company in Indianapolis, Indiana. He has written several technical books and articles and can be reached at sschafer@synergy-tech.com.

# # #

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date