Snapshot/upload utility with GUI

This post describes a short utility that streamlines my workflow in taking screenshots and uploading them to my server. It has a simple GUI and can create both a full-sized and reduced-sized image in a single step.

Longtime readers of this blog will note that I wrote a very similar utility a couple of years ago and decribed it in this post. In those days, I would invoke the script through Quicksilver, using QS’s Run… action to pass parameters—e.g., the file name—to the script. It worked well, but three things pushed me to write a new version:

  1. QS’s Run… action stopped working consistently. It would often disappear entirely from my iBook, leaving me no way to use the script.
  2. I switched from Quicksilver to LaunchBar, in large part because of QS’s quirks. (Here is more complete rundown of my reasons for switching.) But because Launchbar doesn’t allow parameters to be passed to scripts, the older script couldn’t be called from it.
  3. I wanted a graphical user interface so I didn’t have to remember the order of the options.

I call the new program snapftp, and I invoke it through FastScripts. I’ve given it a keyboard shortcut of Control-Option-Command-4, with is similar to the Apple-standard Command-Shift-4 for snapshots of a portion of the screen. The program launches and presents this dialog box.

The filename for the snapshot is entered without the “.png” extension. Three types of snapshot are possible:

  1. A single capture file in the original size.
  2. A single capture file, resized according to the width parameter.
  3. Two capture files, one original and one resized.

By default, the capture files are saved to my Desktop and uploaded to the “images” directory on my server. The checkbox option at the bottom of the window can preclude the upload.

When I click on the Snap button (or press the Return key) the dialog box goes away, and the computer acts very much like it does when I press Command-Shift-4. The biggest difference is that whereas Command-Shift-4 starts in rectangle capture mode, snapftp starts in window capture mode. I prefer starting in window capture mode because I usually want a snapshot of a window. If I need to, I can change to rectangular capture mode by pressing the Space bar.

After the snapshot is made, the capture file appears on my Desktop and, unless told otherwise, is uploaded to my server via FTP. The URL of the uploaded file is put in the Clipboard for later pasting.

If the “Both” option was chosen, there are two capture files. The full-sized capture file has the name I gave it and the resized file has that name with a “-t” appended to it before the “.png” extension (the “t” is for “thumb” even though the resizing may produce an image much bigger than what is normally considered a thumbnail). For example, if I choose “snap” as the file name, “snap.png” will be the full-sized capture file and “snap-t.png” will be the resized capture file. Only the URL to the uploaded version of “snap.png” is put on the Clipboard.

There are several choices for adding a Mac-native graphical user interface to a script. The methods I considered are:

Because it provided everything I needed for snapftp and didn’t force me to learn Interface Builder (which has always seemed exotic and scary to me), I chose Pashua. There was some trial and error involved in positioning the various parts of the GUI, but it wasn’t too painful.

Here’s the snapftp source code:

  1:  #!/usr/bin/python
  2:  
  3:  import Pashua
  4:  import sys, os, shutil
  5:  from subprocess import *
  6:  from ftplib import *
  7:  
  8:  # FTP and local parameters
  9:  host = "leancrew.com"
 10:  baseurl = "http://www.leancrew.com/all-this/images"
 11:  extension = ".png"
 12:  user = "drdrang"
 13:  passwd = "itzaseekret"
 14:  ftpdir = "public_html/all-this/images"
 15:  localdir = os.environ['HOME'] + "/Desktop"
 16:  
 17:  # Dialog box configuration
 18:  conf = '''
 19:  # Window properties
 20:  *.title = Snapshot FTP
 21:  
 22:  # File name text field properties
 23:  fn.type = textfield
 24:  fn.default = snap
 25:  fn.width = 264
 26:  fn.x = 94
 27:  fn.y = 130
 28:  fnl.type = text
 29:  fnl.default = File name:
 30:  fnl.x = 20
 31:  fnl.y = 132
 32:  
 33:  # Radio button group properties
 34:  rb.type = radiobutton
 35:  rb.option = Original
 36:  rb.option = Resized
 37:  rb.option = Both
 38:  rb.default = Original
 39:  rb.x = 94
 40:  rb.y = 52
 41:  rbl.type = text
 42:  rbl.default = Capture:
 43:  rbl.x = 30
 44:  rbl.y = 92
 45:  
 46:  # Resized width text field properties
 47:  rw.type = textfield
 48:  rw.default = 400
 49:  rw.height = 22
 50:  rw.width = 60
 51:  rw.x = 263
 52:  rw.y = 71
 53:  rwl.type = text
 54:  rwl.default = width:
 55:  rwl.width = 50
 56:  rwl.x = 215
 57:  rwl.y = 73
 58:  
 59:  # Local files checkbox properties
 60:  lf.type = checkbox
 61:  lf.label = Local files only
 62:  lf.x = 32
 63:  lf.y = 5
 64:  
 65:  # Default button
 66:  db.type = defaultbutton
 67:  db.label = Snap
 68:  
 69:  # Cancel button
 70:  cb.type = cancelbutton
 71:  '''
 72:  
 73:  # Open the dialog box and get the input.
 74:  dialog = Pashua.run(conf)
 75:  if dialog['cb'] == '1':
 76:    sys.exit()
 77:  
 78:  # Go to the localdir.
 79:  os.chdir(localdir)
 80:  
 81:  # Set the filenames and url.
 82:  fn =  '%s.png' % dialog['fn']
 83:  fnt = '%s-t.png' % dialog['fn']
 84:  url = '%s/%s' % (baseurl, fn)
 85:  
 86:  # Capture a portion of the screen and save it to the file.
 87:  Popen(["screencapture", "-iW", fn], stdout=PIPE).communicate()
 88:  
 89:  # Resize the file if asked to
 90:  if dialog['rb'] == 'Resized':
 91:    Popen(['sips', '--resampleWidth', dialog['rw'], fn],
 92:                      stdout=PIPE).communicate()
 93:  elif dialog['rb'] == 'Both':
 94:    shutil.copy(fn, fnt)
 95:    Popen(['sips', '--resampleWidth', dialog['rw'], fnt],
 96:                      stdout=PIPE).communicate()
 97:  
 98:  # Upload the file unless told not to.
 99:  if dialog['lf'] != '1':
100:    ftp = FTP(host, user, passwd)
101:    ftp.cwd(ftpdir)
102:    ftp.storbinary("STOR %s" % fn, open(fn, "rb"))
103:    if dialog['rb'] == 'Both':
104:      ftp.storbinary("STOR %s" % fnt, open(fnt, "rb"))
105:    ftp.quit()
106:    
107:    # Put the URL of the uploaded file onto the clipboard.
108:    Popen('pbcopy', stdin=PIPE).communicate('%s/%s' % (baseurl, fn))

My original snapshot utility was written in Perl, but I wrote snapftp in Python. Although Python is my current first choice for a scripting language, I would have stuck with Perl—there would have been much less rewriting—except that I found that Python is curiously faster at launching Pashua than Perl is when invoked from FastScripts. I started with a Perl program that just brought up the dialog box, thinking I would add the logic from the old program later. Although this Perl skeleton was fast on my Intel iMac, it took more than 10 seconds to launch Pashua and bring up the dialog box on my iBook G4, which was far too long. I then switched the skeleton program to Python and found that the dialog box came up almost instantly. I have no idea why.

Lines 9-15 provide the FTP and local parameters needed to ensure that the capture files end up where I want them. If you want your own version of snapftp, you’ll have to change these lines.

Lines 18-71 contain the Pashua configuration for the dialog box. One oddity of Pashua is that it wants the element positions given by their left and bottom coordinates rather than left and top. I got goofy looking results until that sunk in.

After getting the information from the user and putting it into the dialog dictionary (Lines 74-76), snapftp starts the built-in screencapture command-line program. The i option puts it in interactive mode, and the W option starts the interaction in window capture mode. As I said earlier, the user can switch to rectangular capture mode by pressing the Space bar.

If the user asked for a resized version of the capture, Lines 90-96 do the resizing with the built-in sips command-line program. The resampleWidth option sets the width of the resized image but keeps the aspect ratio constant. I’ve set this up to resize on the basis of width because I want to make sure the images fit within the content (white) area of the blog, which is sized according the the reader’s default font size. The default width of 400 pixels should make the image fit even for visitors who use a small default font size.

The last section of the program, Lines 99-108, uploads the image(s) to my server via FTP and puts the URL on the Clipboard using the pbcopy command. Of course, this happens only if the “Local files only” checkbox remains unchecked.

Both sips and screencapture are invoked through Python’s relatively new subprocess module. It’s supposed to replace the popen module, which I had just gotten used to. I still prefer Perl’s backticks. And while I’m complaining (mildly), let me say that I also prefer Perl’s FTP library to Python’s. Perl’s way of handling calls to an outside service is to mimic as much as possible the syntax of the service; Python’s way is to make the call using a Pythonesque syntax. While I understand the Python choice, I think it’s an impediment when—as is usually the case—the programmer knows the syntax of the outside service and knows what he would do if he were using that service directly.

I have snapftp saved in my ~/Library/Scripts folder where FastScripts can get at it. The Pashua application is saved in /Applications, as expected, and the Pashua.py Python module is saved in /Library/Python/2.5/site-packages. The module has one line that I feel is unwarranted and that I have commented out. Down near the bottom of Pashua.py, is a loop that looks like this:

for Line in Result:
    print Line
    Parm, Value = Line.split('=')
    ResultDict[Parm] = Value.rstrip()

I think a print command in a library utility that is supposed to read lines is just plain wrong, so I’ve commented out the print Line.

Overall, I’ve found snapftp to be much nicer and easier to use than my old Quicksilver-based solution (even when it worked). The addition of the GUI has not made it slower, and it is, of course, easier to remember the options when they’re laid out in front of you. This may inspire me to add a GUI gloss to some of my other scripts.

Tags:


Formatting flight schedules for trip card

In an earlier post, I described an OmniOutliner template for collecting itinerary information and printing it out on an index card. In this post, I’ll show a simple script that takes flight information from an airline web site and reformats it for the trip card.

One of the tedious parts of making an itinerary is putting the flight information into the right format. The format I like is reflected in the template, which has the departure time first, the arrival time second, and the flight number (or numbers, if it’s a connection) third. The times are right justified and the flight number is left justified.

Unfortunately, the airline web sites have their own ideas about the best way to present flight information, and because their ideas and mine don’t agree, I can’t just copy schedules from their web sites and paste them into my template. I can, however, write a script that does takes the information in the airline’s format and rearranges it into my format. Since I usually fly Southwest, that’s the format I’ve focused on.

Southwest likes to arrange its flight information in tabular form. Each row of the table represents a flight, with the flight numbers first, then its departure and arrival times, then a bunch of other information that I don’t particularly care about right now. What I want to be able to do is select some portion of this table from Southwest’s page

paste it into my OmniOutliner trip card template (using the Paste With Current Style command to avoid overwriting the template’s carefully chosen tab settings)

and then run the script that reformats the data.

Here’s the script. I call it “Reformat SW Schedule” and keep it in ~/Library/Scripts/Applications/OmniOutliner. By putting it there, FastScripts—which I use instead of the usual AppleScript menu—will make it available at the top of its menu when OmniOutliner is the active application.

 1:  #!/usr/bin/python
 2:  
 3:  from appscript import *
 4:  import re
 5:  
 6:  # Get the contents of the selected cell in the front document. It should
 7:  # contain the schedule as it comes from Southwest's site.
 8:  ooFront = app('OmniOutliner').document.get()[0]
 9:  selectedCell = ooFront.selected_rows[0].topic
10:  swSchedule = selectedCell.get()
11:  
12:  # Reformat the Southwest schedule to match the trip card format.
13:  flightInfo = re.compile('^(\S+)\s+(\S+)\s+(\S+).*$', re.MULTILINE)
14:  def format(mo):
15:    infoList = ['\t',
16:                mo.group(2)[:-1],   # departure time, w/o the "m"
17:                '\t',
18:                mo.group(3)[:-1],   # arrival time, w/o the "m"
19:                '\t',
20:                'SW ',
21:                mo.group(1)]        # flight number(s)
22:    return ''.join(infoList)          
23:  sched = flightInfo.sub(format, swSchedule)
24:  
25:  # Put the reformatted schedule back into the selected cell.
26:  selectedCell.set(sched)

The script expects the outline cell with the badly-formatted schedule to be selected. It takes the contents of that cell, reformats it, and replaces the contents with the reformatted version. It will work with one or more rows of information from the Southwest table.

The script uses the appscript Python library to communicate with OmniOutliner. I suppose I could have used AppleScript instead of Python, but this script is mostly text manipulation and I prefer Python’s text handling tools. As with every AppleScript or appscript program, the hardest part is learning the application’s unique set of scripting commands. That’s taken care of in Lines 8 and 9. Line 10 then puts the contents of the cell into a variable, Lines 13-23 transform it, and Line 26 puts the transformed version back into the cell.

If I start using another airline regularly, I’ll probably write a similar reformatter for it.

Tags:


What’s in my Pelican?

Last week I got a red Pelican 1060 Micro Case to carry my breakable and water-sensitive stuff in a saddlebag while I bike back and forth to work. For years I’ve just been wrapping my phone, wallet, USB thumb drive, etc in a plastic bag. This has worked, but when this spring’s stiff breezes blew my bike over a few times—fortunately with the saddlebags empty—I began to think my stuff needed better protection. That my old phone had been replaced by an iPhone gave me further incentive.

Pelican cases are pretty common among people in my business; they protect cameras, microscopes, and other delicate equipment from the abuses of the road. So they were my natural choice when I went looking for a case. The 1060 is the largest of the “micro” cases, which have clear tops and no handle. You fill them with Pelican’s “pick ’n’ pluck” foam, which you can customize to fit the outlines of the equipment you’re carrying.

So here’s how I fill my case. First, I put in my Fisher Space Pen, digital voice recorder, USB thumb drive, and keys. My Camry uses an electronic “Smart Key,” so that thing on the right that looks like a fob actually is the key for my car.

Then I add my iPhone above the Space Pen.

And my watch and earbuds on top of the phone. My earbuds are wrapped up in a short length of inner tube, which keeps them from tangling in my pocket.

Finally, I add my wallet and Levenger Shirt Pocket Briefcase. The SPB is an upscale replacement for the Hipster PDA; I prefer the SPB because it keeps the index cards a bit cleaner and doesn’t have a binder clip that pops off at inopportune times. Despite its name, I carry my SPB in my back pocket.

The wallet and SPB act as top padding and keep everything tight when I close the lid.

I haven’t included links to higher resolution versions of these photos because I took them without a flash to reduce reflections. The exposure times were about 1/5 of a second, so the originals are pretty fuzzy. Fortunately, the reduced sizes here look OK.

Tags:


Best weather webapp for the iPhone

A few days ago, I wrote a post describing a little webapp I’d written that scraped a National Weather Service page to deliver a detailed text forecast to the iPhone. The next morning, I saw that @dsandler had Twittered that night about a Weather Underground service that did the same thing. And more.

The iPhone-optimized page is i.wund.com, and it provides

Some of this is information that could be scraped from the NWS, and I had been thinking about improving my webapp by adding the current conditions and a search field. But there’s no reason for me to go any further; the Weather Underground page gives me everything I want in a compact, fast-loading package. I’ve bookmarked the page for Naperville and added an icon for it to my home screen. Had I known of i.wund.com earlier, I never would have bothered to develop my webapp. At least I learned a bit about Beautiful Soup.

You might wonder why I didn’t learn about the Weather Underground webapp before starting to develop my own. It is, after all, on Apple’s page for popular weather applications. Well, the sad fact is that I’ve been disappointed by so many of the webapps listed on Apple’s iPhone pages that I’ve gotten out of the habit of looking there first. I guess I should get back into the habit.

Tags:


Iraq, April 2008

As usual, I waited a few days before posting the monthly update, because it’s common for the numbers to change. This month’s increase in US deaths is disturbing, of course, but there’s no way to know if it’s the beginning of a trend. Even if the casualty rate drops back to 30-40 in May, it’s still too high for a war with no clear purpose.


Trip card with OmniOutliner

Two or three years ago, I began printing itinerary information on index cards so I had one place to look for flight, hotel, and rental car information. I’ve used this idea for business trips and vacations and it’s very convenient. In this post, I described a script that took its input from a plain text file and formatted and printed a nice looking card. This has worked well, but its one drawback is that its formatting is pretty rigid and I have to remember the best places to put the formatting codes to get good-looking output. If a month or two go by without a trip, I forget and have to look up an old card to remember how to do it.

I’ve been using OmniOutliner (the regular version that was bundled with my Mac) for several writing projects recently, and I’ve come to enjoy it. My trip cards are basically an outline, so it seemed a good fit. My only concern was whether I could coerce it to make a page that fit on an index card. That concern was answered when I looked at its Page Setup… options:

This sheet allowed me to set print margins that would place the outline at the top center, perfect for the way I manually feed index cards into my printer.

I played with the formatting until I got what I wanted, then saved the result as an OmniOutliner template file, which you can download here. Unzip the file, and you’ll get a template that looks like this when it’s opened:

Change or add the information for your trip, print it out on an index card, and you’re good to go.

Update
I’ve improved the formatting for the flight times, and the improvements are reflected in both the screen shot above and in the zip file. The template was made with OmniOutliner 3.6.5, which is the latest version as of this writing. I’m not sure if it will open and print properly on earlier versions.

In tweaking the formatting and print settings of this template, I’ve learned how touchy (that is, buggy) OO is with respect to margin and ruler settings. I’d upgrade to the Pro version if I were confident it didn’t have these bugs. I suppose I should download the Pro version and test it out.

Tags:


Valedictory address

From the AP:

“We have been through a recession, we have been through a terrorist attack, we have been at war, we have had corporate scandals, we have had major natural disasters,” Bush said.

Yeah, that pretty much sums it up.


Better weather forecasts for the iPhone

I’ve come to hate the weather application built into the iPhone. The graphics are cute, but the information is limited: current temperature, highs and lows for today and the next five days, and a little icon that tries to describe the cloudiness/sunniness/raininess/snowiness/fogginess for each day. It’s the icon that gets to me. A spring day in the Midwest can seldom be described with just one picture, because the weather changes too quickly. What I want is something more granular—a forecast for this afternoon, overnight, tomorrow, and tomorrow night. Predictions beyond that are OK, but I don’t really believe them.

So I set about making a web app, geared to the iPhone, that would give me that information. Here’s what it looks like:

It screen-scrapes a National Weather Service page that gives a detailed text forecast for the location of your choice. The full page looks like this:

I suppose I could have grabbed the part of the page that has the icons, but I like having the wind speed and other information that’s in the text forecast.

For my first go-round, I’ve hard-coded my home town into the CGI script, which I’ve called nws-naperville.cgi. It’s a Python program that uses the very cool Beautiful Soup library for scraping the NWS page.

 1:  #!/usr/bin/python
 2:  
 3:  from BeautifulSoup import BeautifulSoup
 4:  import urllib
 5:  
 6:  # Query the NWS site and get the page of results for Naperville.
 7:  f=urllib.urlopen('http://forecast.weather.gov/zipcity.php', urllib.urlencode({'inputstring' : '60565'}))
 8:  page = f.read()
 9:  
10:  # Parse the HTML.
11:  soup = BeautifulSoup(page)
12:  
13:  # Pluck the forecast from below the "Detailed 7-day Forecast" banner.
14:  forecastItem = repr(soup.find('td', { 'width' : '50%', 'align' : 'left', 'valign' : 'top'}))
15:  start = forecastItem.find('<b>')
16:  forecast = forecastItem[start:-17]
17:  
18:  # Construct the page.
19:  print "Content-type: text/html\n"
20:  
21:  print ''' <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html>
22:  <html>
23:    <head>
24:      <title>Naperville Forecast</title>
25:      <meta name="viewport" content="width = device-width" />
26:      <style type="text/css">
27:        body {
28:          font-family: Sans-Serif;
29:          font-size: large;
30:        }
31:        h1 {
32:          font-size: x-large;
33:        }
34:      </style>
35:    </head>
36:  <body>
37:    <h1>Naperville Forecast</h1>'''
38:  
39:  print forecast
40:  
41:  print '''</body>
42:  </html>
43:  '''

Lines 7 feeds one of Naperville’s zip codes to the PHP script that generates the NWS forecast page, and Line 8 gathers up the contents of that page. Beautiful Soup then parses the page on Line 11, and the detailed text forecast is pulled out of the page in Lines 14-16. From that point on, it’s just a matter of generating an HTML output page with the forecast embedded in it. Very simple after Beautiful Soup does the heavy lifting.

I’ve put the script in my cgi-bin directory, and I’ve added a link to it to my iPhone bookmarks. Maybe I’ll get around to creating an icon for the iPhone homepage later.

An obvious improvement to the script would be to generalize it to handle other cities. Fortunately, the NWS’s zipcity.php script can accept “City, State” input as well as zipcodes, which makes searching much easier with no additional work from me. Unfortunately, zipcity.php seems to call different servers depending on where the requested city is. If the requested city is in Arizona, California, Idaho, Montana, Nevada, New Mexico, Oregon, Utah, or Washington, the search is handled by the Western Region Headquarters server (wrh.noaa.gov), which seems to handle redirects and ambiguous city names in ways that are different (and, in my opinion, stupider) than the way the other regional servers work. I had to spend way more time learning about these differences and figuring out how to deal with them than I should have.

Here’s the HTML for simple web page that asks the user to enter a zipcode or city and state. I hope it’s clear from Line 22 that the city and state have to be separated by a comma and the state should be given as a two-letter abbreviation.

 1:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html>
 2:  <html>
 3:    <head>
 4:      <title>Get Forecast</title>
 5:      <meta name="viewport" content="width = device-width" />
 6:      <style type="text/css">
 7:        body {
 8:          font-family: Sans-Serif;
 9:          font-size: large;
10:        }
11:        h1 {
12:          font-size: x-large;
13:        }
14:        input {
15:          font-size: large;
16:        }
17:      </style>
18:    </head>
19:  <body>
20:    <h1>Get Forecast</h1>
21:    <form method="get" action="http://www.leancrew.com/cgi-bin/nws.cgi">
22:      Zip code or City, ST:
23:      <input type="text" name="inputstring" value="" /><br />
24:      <input type="submit">
25:    </form>
26:  </body>
27:  </html>

I call this page forecast.html and have bookmarked this link to it on my iPhone. As you can see on Line 21, it calls a CGI script called nws.cgi:

 1:  #!/usr/bin/python
 2:  
 3:  from BeautifulSoup import BeautifulSoup
 4:  import urllib, os
 5:  
 6:  def followPage(origPage):
 7:    "Recursively follow JavaScript redirects to the final page."
 8:    pos = origPage.find('<script>document.location.replace')
 9:    if pos == -1:
10:      return origPage
11:    else:
12:      start = origPage.find("('", pos) + 2
13:      stop = origPage.find("')", start)
14:      newurl = 'http://www.wrh.noaa.gov' + origPage[start:stop]
15:      f = urllib.urlopen(newurl)
16:      newPage = f.read()
17:      f.close()
18:      return followPage(newPage)
19:  
20:  # Start the script.
21:  
22:  # Where are we getting the forecast from?
23:  place = urllib.unquote_plus(os.environ['QUERY_STRING'].split('=')[1])
24:  
25:  # Query the NWS site and get the page of results.
26:  f=urllib.urlopen('http://forecast.weather.gov/zipcity.php', urllib.urlencode({'inputstring' : place}))
27:  page = f.read()
28:  f.close()
29:  
30:  # If the above URL returns a JavaScript redirect instead of a forecast page,
31:  # follow the redirect.
32:  page = followPage(page)
33:  
34:  # If the search string is ambiguous, choose one of the possiblities presented.
35:  if page.find('More than one match was found') != -1:
36:    soup = BeautifulSoup(page)
37:    # We'll use the first link if we can't find a better fit.
38:    link = soup.h3.nextSibling['href']
39:    linkTags = soup.h3.parent.findAll('a', href=True)
40:    # print linkTags
41:    # Look for an exact match of the city name.
42:    for tag in linkTags:
43:      if tag.string.split(',')[0].lower() == place.split(',')[0].lower():
44:        link = tag['href']
45:        # print tag.string
46:    newurl = 'http://www.wrh.noaa.gov' + link
47:    f = urllib.urlopen(newurl)
48:    page = f.read()
49:    f.close()
50:    # If it's a JavaScript redirect, follow the redirect to get the forecast.
51:    page = followPage(page)
52:  
53:  
54:  # Parse the HTML.
55:  soup = BeautifulSoup(page)
56:  
57:  # Pluck the city and state from the "Point Forcast" line.
58:  cityItem = repr(soup.find('td', {'align' : 'left', 'valign' : 'center'}))
59:  start = cityItem.find('Point Forecast:</b>') + 19
60:  stop = cityItem.find('<br', start)
61:  city = cityItem[start:stop].strip()
62:  
63:  # Pluck the forecast from below the "Detailed 7-day Forecast" banner.
64:  forecastItem = repr(soup.find('td', { 'width' : '50%', 'align' : 'left', 'valign' : 'top'}))
65:  start = forecastItem.find('<b>')
66:  forecast = forecastItem[start:-17]
67:  
68:  # Construct the page.
69:  print "Content-type: text/html\n"
70:  
71:  print '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html>
72:  <html>
73:    <head>
74:      <title>%s Forecast</title>
75:      <meta name="viewport" content="width = device-width" />
76:      <style type="text/css">
77:        body {
78:          font-family: Sans-Serif;
79:          font-size: large;
80:        }
81:        h1 {
82:          font-size: x-large;
83:        }
84:      </style>
85:    </head>
86:  <body>
87:    <h1>%s Forecast</h1>''' % (city, city)
88:  
89:  print forecast
90:  
91:  print '''</body>
92:  </html>
93:  '''

As you can see, it’s about twice as long as nws-naperville.cgi, and virtually all of the additional lines are the idiosyncrasies of the Western Region’s server. The followPage function on Lines 6-18 deals with the (sometimes repeated) JavaScript redirects, and Lines 35-51 deal with the stupid way that server handles ambiguous city names.

[Aside and rant: Here’s an example of the stupidity. If you ask for the forecast for “Phoenix, AZ,” you don’t get the forecast for Phoenix; you’re asked to be more specific. Do you want

This result is, of course, a sad commentary on the Phoenix area, but it’s an even sadder commentary on whoever programmed the Western Region’s server. The other regional servers handle this ambiguity—which isn’t really an ambiguity, since you can’t get any more specific than “Phoenix,” but we’ll let that go—the right way. If you ask for “Chicago, IL,” for example, the Central Region’s server will give you (wait for it) the forecast for Chicago. Yes, it will give you the opportunity to change to Chicago Heights or Chicago Ridge, but only after it’s given you the forecast you asked for, which is almost certainly what you wanted. Really, if you wanted the forecast for South Phoenix, why the hell would you ask for Phoenix?]

I like having both bookmarks on my iPhone. Most of the time I’ll want the Naperville forecast, and it’s much more efficient to go there directly than through a query page. But when I’m out of town, or planning to go out of town, the query page is ready to give me a forecast for anywhere.

Feel free to adapt these scripts for your own use. If you live anywhere but in the Western Region, you should be able to customize the nws-naperville.cgi script by replacing the zipcode in Line 7. If you live in the Western Region and want a script that goes directly to your town’s forecast, you’ll have to customize nws.cgi by changing Line 23 to set place to either your zipcode or your city and state. For example, changing it to

23: place = '98039'

would be appropriate for Bill Gates.

Update
The Weather Underground has a great webapp for the iPhone at i.wund.com. It does everything my app does and more. I wrote a short description of it a few days after this post.

Tags:


A DRM adventure

Today I bought an engineering standard from a standards body that I won’t name (it’s an institute that promulgates national standards in America). I’ve bought many “electronic” standards from this group; they’ve always been plain old PDF files with marginal notes explaining that I’m the only one licensed to use the file, that I’m not allowed to make copies for others, and that I probably shouldn’t even let anyone else read the standard. But there’s never been any DRM to enforce these restrictions.

After downloading the standard I bought today, I soon realized that something was different. When I double-clicked on it, Preview launched, but all the pages were blank and the table of contents in the sidebar was filled with gibberish. OK, I said, let’s try Adobe Reader. I really hate Reader because it takes forever to load and acts like a Windows program by filling the entire screen, but I’ll use it if I have to. Reader launches and soon tells me that it needs a plugin to open the file. It sends me to a website where I can download and install the appropriately-named FileOpen plugin. That done, the PDF opened in Reader and I could start looking through the standard.

Reader soon alerted me that there was a newer version—I had version 7.x, and version 8.x was now available—would I like to download and install the newer version? Being all for progress, I said yes, and soon had Reader 8 in my Applications folder.

When I tried to open the standard in Reader 8, it went through the FileOpen plugin installation and then informed me that it couldn’t open the file because it had already been authorized to “another computer.” That computer was, of course, my computer, the very same computer I was now using, but apparently FileOpen’s authorization is actually applied to a particular application, not to the computer on which the application runs. Since I had authorized the PDF with Reader 7, it could only be opened with Reader 7 (and, presumably, only that particular copy of Reader 7; I’ll bet if I ever had to reinstall Reader 7, the replacement wouldn’t be able to open the file).

So I got on the phone to the standards body and complained, first about the authorization problem and second about forcing me to use Reader instead of Preview. I didn’t, and still don’t, expect anything to come of the second complaint—Windows users seem to think Reader is a fine program. And besides, it’s free, so what’s there to complain about? I did, however, manage to get them to let me to download another copy of the standard, one that I could authorize with Reader 8. The authorization of the new PDF worked—OK, Reader crashed the first time it tried to open the file, but it worked the second time—and the standard was sitting happily on my Desktop.

The first thing I did was print out a copy for safe keeping. In the old days, paper copies were all we had, and I didn’t want to put all my trust in a computer file and program that had already acted flaky. When I hit Command-P, the most amazing dialog box appeared. I didn’t take a screenshot of it at the time, and I’m writing this on a different computer, so I can’t take one now, so a description will have to suffice. (Update: Back at my work computer—here’s a screenshot.)

The dialog box didn’t look like any Print dialog I’d ever seen. There was no opportunity to choose a printer, no option to choose a paper tray, no option for collating or duplex printing, no way to print a range of pages. Here’s what you could choose:

The file would print out, on one side of the paper, on the default printer using the default paper tray. Don’t like that? Talk to the hand.

Apparently, Adobe was scared shitless of Apple’s standard Print dialog, with its dangerous Save as PDF… menu command.

This was a terrible state of affairs. I had just paid for a file that won’t open in Preview, that almost certainly won’t open with the next version of Reader, and that I can’t print the way I want. As I wrote on the Twitter, I was starting to turn into Cory Doctorow. I had to free that PDF!

And with a bit of work, I succeeded. I now have an unlocked, free to copy version of the PDF. I can open it in Preview, I can copy it to my laptop or a thumbdrive, and I can print it on whatever printer I want using whatever options I want. I won’t sell or distribute copies. I’m not interested in piracy; I just want a copy of the standard that’s usable and won’t turn into a pumpkin in a year or two.

I won’t tell you how I unlocked the file (because I am scared shitless of the standards body’s lawyers), but I will remind you of a few things you probably already know:

  1. Printer drivers create temporary files, called spool files, when you issue a print command.
  2. The spool file for a PostScript printer is a PostScript file.
  3. When a printer is paused, the spool file will sit in a particular directory until the printer is unpaused or the print job is deleted.
  4. PostScript files can be turned into PDFs using utilities like Ghostscript and ps2pdf.
  5. Google is your friend.

Godspeed, John Glenn.

Tags:


Time practice sheet for kids

My second-grader has been learning to tell time, and I wanted to make a set of practice sheets for him. I am, of course, far too lazy to draw the clock faces by hand, or even to use a computer-generated sheet of blank faces and then draw the hands on them. So, following the same principles as my earlier math practice sheet, I made up an HTML document that uses JavaScript’s random number generator to generate a set of clock faces.

My son writes the times below each clock and I (or my wife or my fifth-grader) check his work.

Now, when I say that I “made up” this document, what I really mean to say is that I stole some JavaScript from Mathieu ‘P01’ HENRI. He wrote a realtime JavaScript clock program that draws the hands by altering the height and width of some transparent GIFs that contain diagonal lines. I took a good chunk of the JavaScript and some of the CSS and made an HTML file I named “clock-practice.html.” It’s a bit longer than most of the code I post here, and it’s useless without the set of GIFs for the clock face and the hands, so I’ve wrapped them all together in a zip file you can download from here.

One improvement I’ve made over M. HENRI’s work is to draw the hour hand at the correct angle. If you look carefully at his work, you’ll see that he doesn’t include the minute in his calculation of the angle of the hour hand. This has the unfortunate effect of keeping the hour hand at a constant position from xx:00 through xx:59, which would be very confusing to a second-grader. (In M. HENRI’s defense, he’s using a clock to demonstrate a cute way to draw straight lines at any angle in JavaScript. His focus is not on drawing a clock, per se.)

Every time “clock-practice.html” is loaded or reloaded, it redraws the twelve clock faces with new sets of hands. Whenever I need to make a new set of practice sheets, I repeatedly reload and print the page. To assuage my environmental guilt, I print each page on the back of paper that has already been printed on one side and is headed for the recycling bin.

Tags: