blog

blab, blandish, blare, blast, blat, blather, blazen, bleat, blear, bleed, bleep, blench, blend, blight, blink, blit, blitz, bloat, block, blog , bloom, bloop, blossom, blot, blotch, blow, bluff, blunder, blunge, blur, blurt, blush, bluster

Jan 15, 2009

a-python-script-to-make-a-google-earth-kmz

This script makes thumbnails of jpg images using Python PIL, then writes a KML file based on data provided to group photos by location, finally zipping it all up into a self-contained KMZ file.

Taking photographs while traveling is a very common activity. I do it for both research and pleasure. Google Earth is a great way to showcase photographs for locations you visit, or locations in a series along a path. In Google Earth, a discrete location can be set with a placemark, for which you can provide a description. Using well-known HTML methods in the description, you can add links to photographs. Placemarks are described in a KML file. The KML file, along with the images, is zipped up into a KMZ file to complete the job, and the KMZ file can be distributed to others who simply open it to view the locations and photographs.

Here is what Google Earth looks like for a placemark made by this script:

Google Earth Screen Capture

For this project I exported selected photos from iPhoto to a trip images folder at full image size. I used photographs from a trip to Seattle and the Olympic Peninsula of Washington in 2007. I renamed the files with a prefix olympic-001, so that filenames became olympic-003-DSC_0056.jpg, olympic-003-DSC_0057.jpg, etc. for a given location.

In the python file, I typed the prefix (e.g., olympic-003) for each location, the latitude and longitude, and a description into a series of text strings, delimited by the vertical bar (|), which works well as a data delimiter. The script first parses this data into a Python dictionary of location codes and the images for each location.

Then a 'kmz' directory is created.

And into this 'kmz' directory, into a subdirectory called 'files', are written thumbnail images for the photographs, appending '-thumb' to the file basenames.

The KML file is written to contain the placemarks for the locations. The main trick here is to embed HTML tags for the description and thumbnail image links within a CDATA-bounded description section. The thumbnail image links have the form 'files/olympic-003-DSC_0057-thumb.jpg'.

Finally, an os.system() call is made to zip up the KML file and the thumbnail images in the files directory. Here's the actual KMZ file (olympic.kmz) made in this example.

Here's the script, pykmz.py

UPDATE: Added a proper package under revision control so I can fiddle with it.

UPDATE 2: Changed the name from kmzmaker to pykmz, because there is already a Java-based kmzmaker.

And here is the screen output from the script, for this example:

   jeffs-macbook:pykmz jeff$ python pykmz.py 

----------------
Thumbnails...

Wrote: kmz/files/olympic-001-DSC_0003_thumb.jpg
Wrote: kmz/files/olympic-002-DSC_0013_thumb.jpg
Wrote: kmz/files/olympic-003-DSC_0055_thumb.jpg
Wrote: kmz/files/olympic-003-DSC_0056_thumb.jpg
Wrote: kmz/files/olympic-003-DSC_0057_thumb.jpg
Wrote: kmz/files/olympic-003-DSC_0060_thumb.jpg
Wrote: kmz/files/olympic-004-DSC_0070_thumb.jpg
Wrote: kmz/files/olympic-004-DSC_0075_thumb.jpg

----------------
KML file...

Wrote: kmz/olympic.kml

----------------
Zipping KMZ...

  adding: files/ (stored 0%)
  adding: files/olympic-001-DSC_0003_thumb.jpg (deflated 1%)
  adding: files/olympic-002-DSC_0013_thumb.jpg (deflated 3%)
  adding: files/olympic-003-DSC_0055_thumb.jpg (deflated 1%)
  adding: files/olympic-003-DSC_0056_thumb.jpg (deflated 3%)
  adding: files/olympic-003-DSC_0057_thumb.jpg (deflated 6%)
  adding: files/olympic-003-DSC_0060_thumb.jpg (deflated 1%)
  adding: files/olympic-004-DSC_0070_thumb.jpg (deflated 5%)
  adding: files/olympic-004-DSC_0075_thumb.jpg (deflated 2%)
  adding: olympic.kml (deflated 73%)

Zipped: kmz/olympic.kmz
  

Jan 5, 2009

downloading-waypoints-and-tracks-with-garmin-geko

I forgot how to do this and it took me way too long to figure it out again, so here are notes for myself and anyone else who has these GPS units and a Mac.

The Garmin Geko 201/301 GPS Units

Garmin Geko 201

The Garmin Geko 201 is an inexpensive basic unit. Amazon currently has this unit for $91.40. Garmin has nice user manuals available as PDFs, e.g. the Geko 201 user manual .

Garmin Geko 301

The Garmin Geko 301 is also a basic unit, with an integrated compass added. Amazon currently has the unit for $148.95. I bought mine as a reconditioned unit for somewhat less, can't recall.

These units use the etrex data cable, available from Amazon for about $25:

Garmin Etrex Data Cable

The Keyspan USB to Serial Adapter

This is a handy and robust adapter, which Amazon now has for about $25.

Keyspan USA-19HS

The purpose of the device, as its "No Serial Port, No Problem!" descriptor says, is used to connect legacy non-USB devices to a computer. The Keyspan unit comes with a utility called Keyspan Serial Assistant, which simply shows the port name after you install the driver:

Keyspan Serial Adapter

This helps you learn the name of the driver when you are using software needing to communicate with a target device, and to confirm that it is working. Note the names KeySerial1 and USA19H1a2P1.1, which is how the port will show in software setups -- either one works for this Garmin hook-up.

The product identifier for this Keyspan unit is USA-19HS. The HS designation, for High Speed, marks this unit as a replacement for an earlier version of the device. Some software currently lists the HS version as compatible, but not earlier ones.

LoadMyTracks

This free utility software from ClueTrust of Virginia does the job of downloading tracks and waypoints to your Mac. After the Keyspan driver is installed, check it with the Keyspan Serial Assistant, then fire up LoadMyTracks:

LoadMyTracks Screen Capture

Note that I selected Garmin Serial for the protocol, and picked KeySerial1 as the port where I connected the GPS unit -- the GPS unit uses an old-style serial cable, which is connected via the Keyspan USB adapter. You might think, as I did initially, that it would show up on the Mac as a USB connection, but no, it is still a serial connection. For this task, my normal routine is to check Tracks and Waypoints, and KML for the output format, before downloading from the GPS unit to my Mac.

A key point -- the one I forgot: before the GPS unit is recognized and is ready for the connection, you have to navigate on the GPS unit to Menu, then Setup, then Interface, then select Garmin as the protocol. I sometimes connect to my GPS using the NMEA interface for realtime downloading of data using Python and gpsd, and I forgot to switch it back to Garmin, causing me to chase red herrings for too long. Hence, this blog post!

Now to using the data...

Jul 1, 2008

tag-cloud-image

This Python program extends the tag cloud idea to image generation, doing a little filtering on text to remove stop words and common words first. The program uses the Python Imaging Library (PIL) to generate the image. It doesn't do colors yet, but that will be easy to add. This blog post starts development of a proper software program, called TagCloudImage. You follow may development in the software directory.

Tag clouds are handy visual summaries used to represent the frequency of keywords used on a web site. One of the biggest tag clouds I've seen is the one that shows the frequency of categories of software hosted at launchpad.net . You also see one showing keyword frequency on this web site, which is using using the Plone TagCloud product. As you read in TagCloud's description, the frequencies are classed using a power function, following the suggestion by Anders Pearson . This algorithm is also used in TagCloudImage.

I've played around with applying it to long text items. Tag clouds offer promise for quickly grasping what a body of text concerns. I am thinking of applying it to student reports, for example.

I do a good proportion of my work on the Unix command line, so a simple image-generating program is useful. TagCloudImage could be extended as a Plone software add-on. For example, in the Plone environment, a site administrator could periodically create a new snapshot tag cloud image.

TagCloudImage could also be extended with graphics, using simple outlined or filled rectangles as background to text items, or using specialized cloud, bubble, or other backgrounds. The graphics library currently used is the Python Imaging Library (PIL) .

TagCloudImage does the following:

  1. Find all the words and make a list of unique words in the text, removing punctuation.
  2. Remove stop words ., using sources from here and here .
  3. Remove common words, using this source of 1000 or so common words .
  4. Count the words that survive the filtering.
  5. Use the power function algorithm mentioned above to put the words into classes.
  6. Draw and save a tag cloud image showing the words in alphabetical order and in a font and size according to definition of each class.

Here's the code (Everything is included in this file, except for the list of common words ):

   #!/usr/bin/env python

import math
from string import punctuation
import Image 
import ImageDraw 
import ImageFont 

# See also, for the power function to get tag classes:
# http://thraxil.com/users/anders/posts/2005/12/13/scaling-tag-clouds/
# and
# http://behemoth.ccnmtl.columbia.edu/test/clouds/cloud.txt

# For stop words:
# http://www.dcs.gla.ac.uk/idom/ir_resources/linguistic_utils/stop_words

stop_words = """a about above across after afterwards again against all 
almost alone along already also although always am among amongst amoungst 
amount an and another any anyhow anyone anything anyway anywhere are 
around as at back be became because become becomes becoming been before
beforehand behind being below beside besides between beyond bill both
bottom but by call can cannot cant co computer con could couldnt cry
de describe detail do done down due during each eg eight either eleven
else elsewhere empty enough etc even ever every everyone everything
everywhere except few fifteen fify fill find fire first five for
former formerly forty found four from front full further get give go
had has hasnt have he hence her here hereafter hereby herein hereupon
hers herself him himself his how however hundred i ie if in inc indeed
interest into is it its itself keep last latter latterly least less
ltd made many may me meanwhile might mill mine more moreover most
mostly move much must my myself name namely neither never nevertheless
next nine no nobody none noone nor not nothing now nowhere of off
often on once one only onto or other others otherwise our ours
ourselves out over own part per perhaps please put rather re same see
seem seemed seeming seems serious several she should show side since
sincere six sixty so some somehow someone something sometime sometimes
somewhere still such system take ten than that the their them
themselves then thence there thereafter thereby therefore therein
thereupon these they thick thin third this those though three through
throughout thru thus to together too top toward towards twelve twenty
two un under until up upon us very via was we well were what whatever
when whence whenever where whereafter whereas whereby wherein
whereupon wherever whether which while whither who whoever whole whom
whose why will with within without would yet you your yours yourself
yourselves"""

stop_words = stop_words.split()

# For more stop words:
# http://dev.mysql.com/tech-resources/articles/full-text-revealed.html

more_stop_words = """a's, able, about, above, according, accordingly, 
across, actually, after, afterwards, again, against, ain't, all, 
allow, allows, almost, alone, along, already, also, although, always, 
am, among, amongst, an, and, another, any, anybody, anyhow, anyone, 
anything, anyway, anyways, anywhere, apart, appear, appreciate, 
appropriate, are, aren't, around, as, aside, ask, asking, associated, 
at, available, away, awfully, be, became, because, become, becomes, 
becoming, been, before, beforehand, behind, being, believe, below, 
beside, besides, best, better, between, beyond, both, brief, but, by, 
c'mon, c's, came, can, can't, cannot, cant, cause, causes, certain, 
certainly, changes, clearly, co, com, come, comes, concerning, 
consequently, consider, considering, contain, containing, contains, 
corresponding, could, couldn't, course, currently, definitely, 
described, despite, did, didn't, different, do, does, doesn't, doing, 
don't, done, down, downwards, during, each, edu, eg, eight, either, 
else, elsewhere, enough, entirely, especially, et, etc, even, ever, 
every, everybody, everyone, everything, everywhere, ex, exactly, 
example, except, far, few, fifth, first, five, followed, following, 
follows, for, former, formerly, forth, four, from, further, 
furthermore, get, gets, getting, given, gives, go, goes, going, gone, 
got, gotten, greetings, had, hadn't, happens, hardly, has, hasn't, 
have, haven't, having, he, he's, hello, help, hence, her, here, 
here's, hereafter, hereby, herein, hereupon, hers, herself, hi, him, 
himself, his, hither, hopefully, how, howbeit, however, i'd, i'll, 
i'm, i've, ie, if, ignored, immediate, in, inasmuch, inc, indeed, 
indicate, indicated, indicates, inner, insofar, instead, into, 
inward, is, isn't, it, it'd, it'll, it's, its, itself, just, keep, 
keeps, kept, know, knows, known, last, lately, later, latter, 
latterly, least, less, lest, let, let's, like, liked, likely, little, 
look, looking, looks, ltd, mainly, many, may, maybe, me, mean, 
meanwhile, merely, might, more, moreover, most, mostly, much, must, 
my, myself, name, namely, nd, near, nearly, necessary, need, needs, 
neither, never, nevertheless, new, next, nine, no, nobody, non, none, 
noone, nor, normally, not, nothing, novel, now, nowhere, obviously, 
of, off, often, oh, ok, okay, old, on, once, one, ones, only, onto, 
or, other, others, otherwise, ought, our, ours, ourselves, out, 
outside, over, overall, own, particular, particularly, per, perhaps, 
placed, please, plus, possible, presumably, probably, provides, que, 
quite, qv, rather, rd, re, really, reasonably, regarding, regardless, 
regards, relatively, respectively, right, said, same, saw, say, 
saying, says, second, secondly, see, seeing, seem, seemed, seeming, 
seems, seen, self, selves, sensible, sent, serious, seriously, seven, 
several, shall, she, should, shouldn't, since, six, so, some, 
somebody, somehow, someone, something, sometime, sometimes, somewhat, 
somewhere, soon, sorry, specified, specify, specifying, still, sub, 
such, sup, sure, t's, take, taken, tell, tends, th, than, thank, 
thanks, thanx, that, that's, thats, the, their, theirs, them, 
themselves, then, thence, there, there's, thereafter, thereby, 
therefore, therein, theres, thereupon, these, they, they'd, they'll, 
they're, they've, think, third, this, thorough, thoroughly, those, 
though, three, through, throughout, thru, thus, to, together, too, 
took, toward, towards, tried, tries, truly, try, trying, twice, two, 
un, under, unfortunately, unless, unlikely, until, unto, up, upon, 
us, use, used, useful, uses, using, usually, value, various, very, 
via, viz, vs, want, wants, was, wasn't, way, we, we'd, we'll, we're, 
we've, welcome, well, went, were, weren't, what, what's, whatever, 
when, whence, whenever, where, where's, whereafter, whereas, whereby, 
wherein, whereupon, wherever, whether, which, while, whither, who, 
who's, whoever, whole, whom, whose, why, will, willing, wish, with, 
within, without, won't, wonder, would, would, wouldn't, yes, yet, 
you, you'd, you'll, you're, you've, your, yours, yourself, 
yourselves, zero"""

more_stop_words = more_stop_words.split(', ')

stop_words = stop_words + filter(lambda x:x not in stop_words, more_stop_words)

# For common words:
#
# http://www.uri.edu/comm_service/cued_speech/amerpron.html
#
# which says about the source fo the 1000 or so words:
#
#     compiled mostly from words in a special dictionary by Robert 
#     Shaw, The New Horizon Ladder Dictionary, New York: Popular 
#     Library, Inc., 1969, for the United States Information Agency. 
#     The bias favors words in print rather than frequency of spoken 
#     English words. 

common_words = open('common-words.txt').read().split()

excluded_words = stop_words + filter(lambda x:x not in stop_words, common_words)

width = 450
height = width

image_left_margin = 5
image_right_margin = 5

# Find fonts on your system and set this path:
fontPath="/Library/Fonts/arial.ttf"

arial18 = ImageFont.truetype(fontPath,18) 
arial28 = ImageFont.truetype(fontPath,28) 
arial36 = ImageFont.truetype(fontPath,36) 
arial48 = ImageFont.truetype(fontPath,48) 
arial64 = ImageFont.truetype(fontPath,64) 

font_classes = [arial18, arial28, arial36, arial48, arial64]

words_and_counts = {}

for line in open('geojeff_twitter.txt').readlines():
    line = line.strip()
    possible_start_times = ['%02d:' % num for num in range(24)]
    for possible_start_time in possible_start_times:
        if possible_start_time in line:
            line = line[:line.find(possible_start_time)]
    words = line.split()
    for word in words:
        word = word.strip(punctuation)
        if len(word) > 0:
            if word.endswith('\'s'):
                word = word[:-2]
            exclude = False
            try:
                number = int(word)
            except (ValueError, IndexError):
                number = None
            if number:
                exclude = True
            elif 'http' in word:
                exclude = True
            elif len(word) == 1:
                exclude = True
            if not exclude:
                word = word.lower()
                if word not in excluded_words:
                    words_and_counts[word] \
                        = words_and_counts.get(word, 0) + 1

tags = words_and_counts.items()
tags.sort()

levels = 5

def ex_weights(l): return [int(w) for (t,w) in l]

max_weight = max(ex_weights(tags))
min_weight = min(ex_weights(tags))

thresholds = [math.pow(max_weight - min_weight + 1,float(i) \
                / float(levels)) for i in range(0,levels)]

def class_from_weight(w,thresholds):
    i = 0
    for t in thresholds:
        i += 1
        if w <= t:
            return i
    return i

im = Image.new("RGB",(width,height),"#ddd") 
draw = ImageDraw.Draw(im) 

textsizes = [draw.textsize('Boy', font_classes[0]),
             draw.textsize('Boy', font_classes[1]),
             draw.textsize('Boy', font_classes[2]),
             draw.textsize('Boy', font_classes[3]),
             draw.textsize('Boy', font_classes[4])]

x = 0
# fix width to at least fit the longest item
for (t,w) in tags:
    c = class_from_weight(w,thresholds) - 1
    font = font_classes[c]
    word_w,word_h = draw.textsize(t, font=font)
    if (x + word_w) > width:
        width = x + word_w

lines = []
current_line = []
x = 0
y = 0
word_left_margin = 0
max_font_index = 0
for (t,w) in tags:
    c = class_from_weight(w,thresholds) - 1
    font = font_classes[c]
    word_w,word_h = draw.textsize(t, font=font)

    # set the x position
    if len(current_line) == 0:
        x = image_left_margin
        word_left_margin = 0
    else:
        previous_word_w,previous_word_h = current_line[-1][3]
        word_left_margin = int((float(previous_word_w) * 0.05 \
                                   + float(word_w) * 0.05) / 2.0)
        x = x + previous_word_w + word_left_margin

    if (x + word_w) < width:
        current_line.append((t, c, (x,y), (word_w,word_h), 
                             font, word_left_margin))
        if c > max_font_index:
            max_font_index = c
    else:
        lines.append(current_line)
        y = y + textsizes[max_font_index][1]
        max_font_index = 0
        current_line = []
        x = image_left_margin
        word_left_margin = 0
        current_line.append((t, c, (x,y), (word_w,word_h), 
                             font, word_left_margin))
        if c > max_font_index:
            max_font_index = c

image_width = 0
image_height = 0
lines_adjusted = []
for line in lines:
    width_for_line = 0
    max_height_for_line = 0
    for word_data in line:
        x,y = word_data[2]
        if x > width_for_line:
            width_for_line = x
        word_left_margin = word_data[5]
        word_w,word_h = word_data[3]
        width_for_line += word_left_margin + word_w
        if word_h > max_height_for_line:
            max_height_for_line = word_h
    line_adjusted = []
    for word_data in line:
        word_w,word_h = word_data[3]
        if word_h != max_height_for_line:
            x,y = word_data[2]
            y_adjusted = y + int((float(max_height_for_line) \
                                    - float(word_h)) * 0.5)
            line_adjusted.append((word_data[0], word_data[1], 
                                  (x,y_adjusted), word_data[3], 
                                  word_data[4], word_data[5]))
        else:
            line_adjusted.append(word_data)
    lines_adjusted.append(line_adjusted)
    if image_width < width_for_line:
        image_width = width_for_line
    image_height += max_height_for_line

im = Image.new("RGB",(image_width,image_height),"#ddd") 
draw = ImageDraw.Draw(im) 
for line in lines_adjusted:
    for word_data in line:
        word = word_data[0]
        x,y = word_data[2]
        font = word_data[4]
        draw.text((x,y),word,font=font,fill="black") 

im.save("tagcloud.png") 

for line in lines_adjusted:
    for word_data in line:
        print word_data[0]
  

And here is the image that is produced when I apply the program to my first 42 Twitter updates, to summarize the subjects I had been tweeting about (451x2508 pixels!):

tagcloud.png

Jun 10, 2008

my-spore-creation-li..ih..ihh..ihhhves

Want to try your luck at evolution? Want to spend countless hours playing a computer game? Want to have some fun and laughter?

Meet BiSpike

BiSpike-Cell From humble beginnings,

BiSpike-Cell-2 To a terrestrial sphere,

BiSpike-Biped Walked a curious creature,

BiSpike-Biped-Beaky With sharp beak to fear.

BiSpike-Biscythe It was scythed, and stood in a linebacker pose,

BiSpike-Biscythe-Snarl Then taller and longer, with fangs on the nose,

BiSpike-Regressed-Unieye Then squatty again, in tri-eyed regression,

BiSpike-Regressed-Unieye-Antennae Came antennae splayed wide,

BiSpike-Regressed-Unieye-Antennae-Brown And a colored expression,

BiSpike-Regressed-Unieye-Antennae-Brown-Flirt To gawky composure and a very long neck,

BiSpike-Regressed-Unieye-Antennae-Brown-Settled came eyes all around,

BiSpike-Regressed-Unieye-Antennae-Brown-Mustache And a mustache on deck,

BiSpike-Regressed-Unieye-Antennae-Brown-Fez Bedazzling at present, with feather and fez,

Ready for anything, going off the rez!

BiSpike thrives in the wild world of Spore ,

a cool game by Maxis you cannot ignore.

Learning evolution is unbelievably virtuous,

Go out now and unabashedly purchase it! :)

May 4, 2008

posting-map-locations-to-a-plone-web-site

If you use the Maps add-on software in Plone, you may wish to use a Python script to post locations from a list of latitude and longitude and other data for the locations. The script described here does this for locations of birding spots and hiking trails in southeast Texas. The script was used on the Golden Triangle Audubon Society web site.

This script adds map locations to a Plone web site that is running the Maps add-on software . In this example, we add locations for birding locations and hiking trails in southeast Texas, for the Golden Triangle Audubon Society's web site . Data for these locations includes the county, short name (the name to be used within Plone, as part of the web address of the location), title (name of the location), and latitude and longitude coordinates. This location data was actually created on an earlier version of the web site, and it was saved off for recreation later, which this script does.

The data was pasted into a Python file as line data. You will see the data contained within a triple-quoted string. This string is split into lines of data by use of Python's string.split() method, which we tell to split on the newline, the end-of-line marker, '\n'.

Once we have the data as lines, we loop through the lines and call split() again, this time telling it to split each line on commas. That gives us the data broken out into variables: county, short_name, title, latitude, and longitude.

We loop through one time and add the data to a dictionary using the county name for the key. The values stored in the dictionary, which is called counties_and_locations, is the rest of the data put in a tuple. A tuple is a list of data variables marked by containing parentheses. Data in a tuple is immutable, or unchanging, so this is a fitting storage choice for this static data.

The script will create folders for each county, and will create separate map location objects in the county folders. First , we call Plone's invokeFactory() method (or, the underlying Zope's) to create a high-level folder to contain the county folders. Then we sort the county names, and loop through them, using them in their role as keys to the data dictionary. For each county, we first use invokeFactory() method to create a folder for the county. Then we get the list of locations in the county, which is stored in the dictionary. We sort the location data by the title, so that our locations will be added to the county folder in alphabetical order. Then we are ready to loop through the locations for a given county and call invokeFactory(), this time to create Location objects, passing the location's short_name, title, and latitude and longitude values.

And that's it. We could have the script publish the items, but I did that manually by clicking the main containing folder and using the Contents tab to publish the whole batch of folders and locations in one fell swoop.

I also set the Display for each county folder to be "Map View" -- the desired result, a Google map. For example:

Jefferson County, Texas Birding Locations

Here's the code of the Python script:

   context.invokeFactory(id='gtas-locations', type_name='Folder', title='Locations By County')
gtasMapFolder = getattr(context, 'gtas-locations')

# Each line of the data has county, short name for location, location name, latitude, longitude

gtas_data_string = """Angelina County, boykin-springs, Boykin Springs, 31.0891040579  -94.2649269104  
Hardin County, firetower-road, Firetower Road, 30.4753075972 -94.2317962646 
Hardin County, kirby-nature-trail, Kirby Nature Trail, 30.464877 -94.340544 
Hardin County, turkey-creek-trail, Turkey Creek Trail at Gore Store Road, 30.5208642936 -94.3437194824 
Jasper County, ebenezer-park, Ebenezer Park, 31.0739613524 -94.128112793 
Jasper County, sundew-trail, Sundew Trail, 30.552357 -94.411697 
Jasper County, hen-house-unit, Hen House Ridge Unit of MDSP, 30.8459421801 -94.1698265076 
Jasper County, walnut-ridge, Walnut Ridge Unit of MDSP, 30.8631840565 -94.1792678833 
Jasper County, jasper-state-fish-hatchery, Jasper State Fish Hatchery, 30.9466969075 -94.1282844543 
Jasper County, sam-rayburn-reservoir-dam, Sam Rayburn Reservoir Dam, 31.0652862889 -94.0881156921 
Jasper County, sandy-creek-park, Sandy Creek Park, 30.8132183064 -94.1634750366 
Jefferson County, cattail-marsh, Cattail Marsh, 30.007831 -94.143133 
Jefferson County, hillebrandt-bayou-watershed, Hillebrandt Bayou, 29.9285685604 -94.1104745865 
Jefferson County, mcfaddin-nwr, McFaddin National Wildlife Refuge, 29.6685150554 -94.074382782 
Jefferson County, northwest-jefferson-county, Northwest Jefferson County, 29.9580578427 -94.3557357788 
Jefferson County, pilot-station-road, Pilot Station Road, 29.6941668206 -93.852596283 
Jefferson County, pleasure-island, Pleasure Island, 29.8518853121 -93.9424610138 
Jefferson County, sabine-woods, Sabine Woods, 29.6955 -93.958325 
Jefferson County, sea-rim-state-park, Sea Rim State Park, 29.675674346 -94.0439987183 
Jefferson County, taylor-bayou-watershed, Taylor Bayou Watershed, 29.8806251864 -94.2571806908 
Jefferson County, texas-point-nwr, Texas Point National Wildlife Refuge, 29.7078848448 -93.9207458496 
Jefferson County, tyrell-park, Tyrrell Park, 30.0245811058 -94.1486048698 
Newton County, bon-wier, Bon Wier, 30.7393275156 -93.6443710327 
Newton County, wild-azalea-canyons, Wild Azalea Canyons, 30.8932393995 -93.6531257629 
Orange County, bessie-heights, Bessie Heights, 30.0439842247 -93.9228057861 
Orange County, claiborne-west-park, Claiborne West Park, 30.13111667 -93.9212822914 
Orange County, baileys-fish-camp, Bailey's Fish Camp, 29.9845275279 -93.8405799866 
Orange County, tony-houseman-sp, Tony Houseman S. P. (Blue Elbow Swamp), 30.1228950059 -93.7134647369 
San Augustine County, san-augustine-park, San Augustine Park, 31.2023771513 -94.0707778931 
San Augustine County, turkey-hill-wa, Turkey Hill Wilderness Area, 31.3652171566 -94.1710281372 
San Augustine County, jackson-hill-park, Jackson Hill Park, 31.28500592 -94.310760498 
San Augustine County, harvey-creek-park, Harvey Creek Park, 31.2142695783 -94.2644119263 
San Augustine County, powell-park, Powell Park, 31.1137976621 -94.1130924225 
Tyler County, beechwoods-trail, Beechwoods Trail, 30.731212296 -94.2309379578 
Tyler County, campers-cove-park, Campers Cove Park, 30.8220636965 -94.2022705078 
Tyler County, cherokee-unit, Cherokee Unit of MDSP, 30.8539003543 -94.2149734497"""

counties_and_locations = {}
for line in gtas_data_string.split('\n'):
    line = line.strip()
    county,short_name, title,location = line.split(',')
    county = county.strip()
    short_name = short_name.strip()
    title = title.strip()
    lat,lon = location.split()
    lat = float(lat)
    lon = float(lon)

    if county in counties_and_locations:
        counties_and_locations[county].append((short_name, title, lat, lon))
    else:
        counties_and_locations[county] = [(short_name, title, lat, lon)]

# Sort the county names
counties = counties_and_locations.keys()
counties.sort()

for county in counties:
    county_folder_short_name = '-'.join([word.lower() for word in county.split()])
    gtasMapFolder.invokeFactory(id=county_folder_short_name, type_name='Folder', title=county)
    countyFolder = getattr(gtasMapFolder, county_folder_short_name)

    # Sort the data on title, which is the third data item
    counties_and_locations[county].sort(lambda x,y:cmp(x[2],y[2]))

    for location_data_item in counties_and_locations[county]:
        short_name,title,lat,lon = location_data_item
        countyFolder.invokeFactory(id=short_name, title=title, 
                                   type_name='GeoLocation', geolocation=(lat,lon))
        newLocation = getattr(countyFolder, short_name)
        print newLocation.geolocation

return printed
  

To use the script, I pasted it into a Script (Python) using the Plone web site's ZMI (Zope Management Interface), putting the script in the portal_skins/custom folder, the typical place to put such scripts. Then I invoked the script in my home folder on the web site first, to trouble shoot it. I had to do some iterative debugging/copying/re-invoking, as usual, but once it was working, I then invoked it in the main map folder for the site. Putting scripts in portal_skins/custom is handy that way, because you can call them from anywhere on the Plone site (at any folder location).