Compare with Previous | Blame | View Log
#!/usr/bin/env python # forgottenislanderbot - makes a DSL coverage map from Bell Aliant's website. # Copyright (c) 2010 Art Ortenburger # # 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 2 # of the License, or 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, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # fib.py # by Art Ortenburger IV <utrrrongeeb a users , sf , net> # written 2010-01-25 - # # forgottenislanderbot -- unified interface # # Provides all forgottenislanderbot functions from # one command-line interface, using standard-form # command-line options with flexibility. # import sys # for argument input and stream buffer flushing import optparse # for command and option parsing import sqlite3 # for all database operations import zipfile # for KMZ output import StringIO # for KML output import traceback # maybe used once for debugging import robotparser # Robot Exclusion Standard; user may override import urllib # import httplib # loading web page import time # sleeping between requests import random # randomizing sleeping, etc. sd = None # Sqlite Database sdc = None # Sqlite Database Connection # names for each DSL status dslnames = ["ERROR","EMPTY","NODSL","BASIC","ULTRA","TOTAL"] # collected statistics stored here statistics = [0, 0, 0, 0, 0, 0] kmlbuffer = None # KML file to write kmlIconHighlitScale = 1.1 # coefficient for highlit icon size in KML GMIconURLs = ["http://maps.google.com/mapfiles/kml/pushpin/purple-pushpin.png", "http://maps.google.com/mapfiles/kml/pushpin/wht-pushpin.png", "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png", "http://maps.google.com/mapfiles/kml/pushpin/ltblu-pushpin.png", "http://maps.google.com/mapfiles/kml/pushpin/grn-pushpin.png"] GMIconHotspot = [ 20, 2 ] # hostname of Bell Aliant server ba_host = "productsandservice.bellaliant.net" # webpage which so conveniently does a whole address check with one request. Such a shame it won't take the Range: bytes= header.... ba_handler = "/PS/pe/english/productsandservices/qualificationFullAddressCheck.do" def main(): """ main UI function for forgottenislanderbot""" print sys.argv[0],"""(forgottenislanderbot unified interface) Version 0.1 Copyright (c) 2010 Art Ortenburger. Licensed under the GNU GPL; no warranty.""" # ease argument parsing. The two parsers make laziness look elegent -- # you can run "fib.py --help" to get a command list, # or "fib.py --all --help" to get an option list. # usage strings must be set appropriately. # later: add --version? # command parser -- only parses one arg. cmdParser = optparse.OptionParser(usage="") # option parser -- parses anything else optParser = optparse.OptionParser(usage="") # commands. At the moment, only one command is allowed, but by using # the "append" or "callback" actions I might make it multi-command later. # later: single-char options? cmdParser.add_option("--all", action="store_true", dest="auto", default=False, help="do everything -- get civic addresses, run bot, and produce KMLs") cmdParser.add_option("--newdb", action="store_true", dest="newdb", default=False, help="download civic addresses and make a new, empty database") # later: add commands to only download, only make/merge cmdParser.add_option("--crawl", action="store_true", dest="crawl", default=False, help="query the Bell Aliant server, finding DSL statuses") cmdParser.add_option("--kml", action="store_true", dest="kml", default=False, help="generate a KML overlay from a database") cmdParser.add_option("--stats", action="store_true", dest="stats", default=False, help="print the totals of each DSL status") cmdParser.add_option("--list", action="store_true", dest="listdb", default=False, help="print every selected address") # later: add merge/update, diff, strip commands # options. All options are...optional. # later: single-char options?, better help # Global options: optParser.add_option("--db", action="store", dest="db", default="database.sqlite", help="SQlite database file to work with. Default: database.sqlite", metavar="FILE") # there are NOT going to be individual dslmasks, sparsities, maxrows, latlons, # custom SQLs, endofroads, or nocities for --crawl, --stats, and --kml. Period. # anyone needing such functionality will have to use a shell script and multiple # invocations, or modify the code themselves. optParser.add_option("--dslmask", action="store", dest="dslmask", type="string", default=None, metavar="MASK", help="DSL types to process; y/n for each of EMPTY, NODSL, BASIC, & ULTRA. Overrides defaults: crawl=ynnn, stats=yyyy, kml=nyyy") optParser.add_option("--sparsity", action="store", dest="sparsity", type="int", default=None, metavar="INT", help="minimum difference in civic numbers along a road to process. Overrides defaults: crawl=1, stats=1, kml=40") optParser.add_option("--nocities", action="store_true", dest="nocities", default=False, help="don't process addresses in Charlottetown or Summerside") optParser.add_option("--max", action="store_true", dest="maxrows", default=False, help="process more addresses when using a >1 sparsity.") optParser.add_option("--always-get", action="store", dest="alwaysget", default="first", metavar="HOUSE", help="ignoring sparsity, which address on the end of a road to always get. Values: 'none', 'first' (default), 'last', 'both'.") optParser.add_option("--sql-where", action="store", dest="sqlwhere", type="string", default=None, metavar="SQL", help="custom SQL SELECT conditions. Inserted directly after WHERE. Be careful!") optParser.add_option("--coord-rect", action="store", dest="coordrect", type="string", default=None, metavar="latA,longA,latB,longB", help="latitude/longitude coordinates limiting processing to addresses within the rectangle") # newdb, merge options: optParser.add_option("--tsv", action="append", dest="tsv", type="string", default=None, metavar="TSV-FILE", help="TSV database of civic addresses to add to SQlite database. Can appear multiple times.") # crawler-bot options: optParser.add_option("--override-robot-exclusion-standard", action="store_true", dest="ores", default=None, help="override a robots.txt, instead of asking") optParser.add_option("--quiet-fail", action="store_false", dest="ores", default=None, help="don't ask about a robots.txt -- just exit") optParser.add_option("--delay", action="store", type="float", dest="delay", default=1.0, metavar="SECONDS", help="time to sleep between single-threaded requests to Bell Aliant server. Can be float. Default: 1.0") optParser.add_option("--no-connect", action="store_true", dest="noconnect", default=False,help="Don't reconnect HTTP connection between requests. Experimental, will probably fail.") optParser.add_option("--useragent", action="store", dest="useragent", default="forgottenislanderbot", metavar="NAME", help="user-agent for the bot to represent itself with. Default: %default") optParser.add_option("--pause-every", action="store", dest="pauseblock", type="int", default=20, metavar="QUERIES", help="number of requests to run between database-commit / kill-opportunity sleep") optParser.add_option("--pause-time", action="store", dest="pausetime", type="float", default=3, metavar="SECONDS", help="time to sleep at an intermittent pause") # stats options: # kml options: optParser.add_option("--map",action="store",type="string",dest="kmlfile", default="dslmap.kml",metavar="KMLFILE",help="file to write KML to. Will be overwritten. Default: dslmap.kml") optParser.add_option("--globe",action="store",type="string",dest="kmlglobe", default=None, metavar="[GE|NWW]", help="virtual-globe to optimize KML for") optParser.add_option("--civic-addresses-in-kml", action="store_true", dest="keepaddrs", default=False, help="Label icons in KML by civic address. May raise privacy concerns, file size, lag.") optParser.add_option("--no-highlight", action="store_true", dest="nohighlight", default="False", help="Disable highlightable icons in KML. Experimental.") optParser.add_option("--kmz", action="store_true", dest="kmz", default=False, help="Make a compressed KMZ from the KML.") optParser.add_option("--iconsize", action="store", type="float", dest="iconsize", default="1.0", metavar="FLOAT", help="Size of KML icons. Default: 1.0") optParser.add_option("--icontheme", action="store", type="string", dest="icontheme", default="GM", metavar="[NAME|ante*post.png]", help="Icons to use in KML. Either builtin, like 'GM', or external, like " "'dsl-*.png', where '*' comprises 'ERROR', 'EMPTY', 'NODSL', 'BASIC', and 'ULTRA', " "in separate files. It can also be a URL.") optParser.add_option("--iconhotspot", action="store", type="string", dest="iconhotspot", default=None,metavar="x,y", help="Pixel coords in an icon where it is aligned over the lat/long coords.") optParser.add_option("--iconcolours",action="store", type="string", dest="iconcolours", default=None, metavar="LIST",help="Colours to tint KML icons, " "as comma-separated list of five colours like 'bf0000ff', in 'AABBGGRR' format.") # csv options: optParser.add_option("--csvfile", action="store", type="string", dest="csvfile", default="coords.csv", metavar="CSVFILE", help="file to write CSV to. Will be overwritten. Default: %default") # ui options: optParser.add_option("--pipeable", action="store_true", dest="pipeable", default=False, help="Changes stdout flushing behavior. May cause problems on some systems. Not implemented.") optParser.add_option("--verbose", action="store_true", dest="verbose", default=False, help="Prints additional information. Not implemented.") optParser.add_option("--debug", action="store_true", dest="debug", default=False, help="Prints massive amounts of debug information. Implemented as necessary.") if len(sys.argv) <= 1: # no command, can't do anything cmdParser.print_help() exit(0) # parse command (cmd,cmdargs) = cmdParser.parse_args([sys.argv[1]]) # parse options (opt, optargs) = optParser.parse_args(sys.argv[2:]) # extract and parse dslmask and sparsity -- the first time dslmask = (opt.dslmask and [parseDSLMask(opt.dslmask)] or [None])[0] sparsity = (opt.sparsity and [opt.sparsity] or [None])[0] # individual mode dslmasks and sparsities crawldslmask = (dslmask or [True,False,False,False]) kmldslmask = (dslmask or [False,True,True,True]) csvdslmask = (dslmask or [False,True,False,False]) statsdslmask = (dslmask or [True,True,True,True]) listdbdslmask = (dslmask or [False,True,False,False]) crawlsparsity = (sparsity or 1) kmlsparsity = (sparsity or 40) csvsparsity = (sparsity or 1) statssparsity = (sparsity or 1) listdbsparsity = (sparsity or 1000) # change default name to .kmz if necessary outkml = ((opt.kmz and opt.kmlfile == "dslmap.kml") and ["dslmap.kmz"] or [opt.kmlfile])[0] # take action depending on mode try: if cmd.auto: # do the whole thing initdb(opt.db) newdb(sdc, opt) crawl(sdc, crawldslmask, crawlsparsity, opt) # loop, checking stats, to repeat crawl kml(sdc, kmldslmask, kmlsparsity, opt, outkml) stats(sdc, statsdslmask, statssparsity, opt, True) elif cmd.newdb: initdb(opt.db) newdb(sdc, opt); elif cmd.crawl: initdb(opt.db) crawl(sdc, crawldslmask, crawlsparsity, opt) elif cmd.kml: initdb(opt.db) kml(sdc, kmldslmask, kmlsparsity, opt, outkml) elif cmd.stats: initdb(opt.db) stats(sdc, statsdslmask, statssparsity, opt, True) elif cmd.listdb: initdb(opt.db) listdb(sdc, listdbdslmask, listdbsparsity, opt) else: cmdParser.print_help() except Exception, errdet: print "Failing. Command could not be completed. Data may not have been saved." print "Error:",errdet traceback.print_exc() done(1) done() def done(errcode=0): """ Exits safely, committing, flushing, and closing anything that needs it.""" try: sdc.close() except: pass try: sd.commit() sd.close() except: pass exit(errcode) def initdb(dbn): # opens the database """ opens sqlite database for use by FIB UI.""" try: globals()['sd'] = sqlite3.connect(dbn) globals()['sdc'] = sd.cursor() except Exception, errdet: print "Failing: couldn't open database ",dbn,": ", errdet print sys.exc_info()[0] done(1) def newdb(dc, opt): # downloads civic address databases, and merges them into a new database pass def crawl(dc, opt): # real bot part -- queries pass def kml(dc, dslmask, sparsity, opt, outkml): # generates KML file """ Generates a KML file from a database. dc: sqlite3.Cursor to FIB database. dslmask: 4 bools in list. Whether to include in KML output each of EMPTY, NODSL, BASIC, & ULTRA categories. Usually [False,True,True,True] sparsity: int; minimum civic-number distance between houses to output. This is to avoid slowing Google Earth to a crawl with 68023 placemarks. opt: key-value list of miscellaneous options. Documentation is elsewhere. outkml: string; path to KML file to write. Will be overwritten.""" globals()['kmlbuffer'] = StringIO.StringIO() kmlbuffer.write(kmlMakeHeader(outkml,opt)) processDB(dc,kmlItemAction,dslmask,sparsity,opt) kmlbuffer.write(kmlMakeFooter(outkml,opt)) kmlfile = None kmzzip = None try: kmlfile = open(outkml,"wb") if opt.kmz: # write a zipfile kmzzip = zipfile.ZipFile(kmlfile,'w',zipfile.ZIP_DEFLATED) kmzzip.writestr("doc.kml",kmlbuffer.getvalue()) # doc.kml is the special name required by Google Earth kmzzip.close() else: # write normal KML kmlfile.write(kmlbuffer.getvalue()) kmlfile.flush() kmlfile.close() except Exception, errdet: print "Error writing KML:",errdet print sys.exc_info()[0] try: kmzzip.close() except: pass try: kmlfile.flush() kmlfile.close() except: pass kmlbuffer.close() def stats(dc, dslmask, sparsity, opt, printtable): # prints totals of each DSL type """ Prints statistics from civic address database. dc: sqlite3.Cursor to FIB database dslmask: 4 bools in list. Whether to sum in output each of EMPTY, NODSL, BASIC, & ULTRA categories. Usually [True,True,True,True] sparsity: int; minimum civic-number distance between houses to output. This is to find out what a given sparsity will do to the crawler or Google Earth. For totals, use 1. opt: key-value list of miscellaneous options. Documentation is elsewhere. printtable: bool; whether to print table, or just return the values.""" # reset statistics totals for i in range(len(statistics)): statistics[i] = 0 # use utility function to loop through database processDB(dc,statsItemAction,dslmask,sparsity,opt) if printtable: print # newline needed # print totals table for i in range(len(dslnames)): print dslnames[i],": ",statistics[i] return statistics def listdb(dc, dslmask, sparsity, opt): """ Prints addresses from civic address database. dc: sqlite3.Cursor to FIB database dslmask: sparsity: int; minimum civic-number distance between houses to output. This is to find out what a given sparsity will do to the crawler or Google Earth. For totals, use 1. opt:""" processDB(dc,listItemAction,dslmask,sparsity,opt) sys.stdout.flush() def processDB(dc, actionfunc, dslmask, sparsity, opt): """ Loops through SQlite database, selecting rows based on parameters, and calling the specified function for each selected row. dc: sqlite3.Cursor to civic address database from fib-cadb2sql.py actionfunc: function to call for selected addresses. Must accept these parameters: sqlite3.Cursor town (string) road (string) civic number (int) dsl status of address (int) longitude (float) latitude (float) option set (key:value list) dslmask: 4 bools in a list. Whether to call actionfunc() for rows in the EMPTY, NODSL, BASIC, or ULTRA categories. sparsity: int; minimum civic-number difference between houses to process. To get every house, enter 1. For about one house per road, enter 10000. opt: key-value list of miscellaneous options. Documentation is elsewhere.""" # parse the lat/lon rectangle, if needed coordrect = (opt.coordrect and True) if coordrect: coordmaskstr = opt.coordrect.split(',') coordmask = [0.0, 0.0, 0.0, 0.0] # will not allow invalid args to fail this, but will complain if len(coordmaskstr) < 4: print "invalid coordinate rectangle",coordmaskstr try: # same here for i in range(4): coordmask[i] = float(coordmaskstr[i]) except: print "invalid coordinate rectangle",coordmaskstr coordrect = False # parse whether to always get the first house, the last house, both, or neither on a road alwaysfirst = (opt.alwaysget == "first") or (opt.alwaysget == "both") alwayslast = (opt.alwaysget == "last") or (opt.alwaysget == "both") # query the sqlite database for everything. The blob at the end is for custom SQL conditions. dc.execute("select town,road,civic,dsl,long,lat from dsl"+(opt.sqlwhere and [" where "+opt.sqlwhere] or [""])[0]+" order by town,road,civic") # town, road... : data for presently-processed address # last___ : data for previous address as pertains to determining sparsity # lastok___ : data for previous selected-in-all-but-sparsity address, for last-on-road feature town = road = lasttown = lastroad = lastoktown = lastokroad = "" # strings civic = status = lastcivic = laststatus = lastokcivic = lastokstatus = 0 # ints lon = lat = lastlon = lastlat = lastoklon = lastoklat = 0.0 # floats notgot = lastoknotgot = False # bools -- used exclusively for last-on-road feature. Name translates to 'last okay address -- not been gotten?', meaning 'selected' # this block will take a lot of ram due to fetchall() rather than fetchone(). # Shouldn't be hard to fix if needed, though. Current-db ram req: 6 MB max? for cca in dc.fetchall(): # current civic address town,road,civic,status,lon,lat,okay = cca[0],cca[1],int(cca[2]),int(cca[3]),float(cca[4]),float(cca[5]),False # casts here are important # skip cities -- currently only Ch'town and Summerside -- if requested in opt if opt.nocities and ( town == "CHARLOTTETOWN" or town == "SUMMERSIDE" ): continue # skip addresses outside of the latitude-longitude rectangle, if requested in opt if coordrect: if not ((min(coordmask[0],coordmask[2]) < lat < max(coordmask[0],coordmask[2])) and (min(coordmask[1],coordmask[3]) < lon < max(coordmask[1],coordmask[3]))): # check whether address is within coord rectangle continue # apply the dslmask. If status doesn't match, check maxrows # on whether to set last___ vars, to determine whether a # maximum number of addresses will be gotten, or a regular # sparseness will be maintained. if not dslmask[((status < 0) and [0] or [status])[0]]: # treat errors (-1) as empty (0). # doesn't match up -- ignore if not opt.maxrows: lasttown,lastroad,lastcivic,laststatus,lastlon,lastlat = town,road,civic,status,lon,lat continue; try: # loop must not crash notgot = False # by default, assume the address was selected # on the same road, make sure to skip houses for sparseness if town == lasttown and road == lastroad: if (civic - lastcivic) >= sparsity: # sparseness check actionfunc(dc,town,road,civic,status,lon,lat,opt) else: # if sparseness check discards an address, put it in the running to be selected for being last on a road: notgot = True lastoktown,lastokroad,lastokcivic,lastokstatus,lastoklon,lastoklat = town,road,civic,status,lon,lat else: # on a new road, decide whether to get the first house, or the last house on the previous road if alwaysfirst: actionfunc(dc,town,road,civic,status,lon,lat,opt) if alwayslast and (lastroad != "") and lastoknotgot: actionfunc(dc,lastoktown,lastokroad,lastokcivic,lastokstatus,lastoklon,lastoklat,opt) except Exception, errdet: print "> error < :",errdet print sys.exc_info()[0] lasttown,lastroad,lastcivic,laststatus,lastlon,lastlat = town,road,civic,status,lon,lat lastoknotgot = notgot # end of for loop if alwayslast and (lastroad != "") and lastoknotgot: # this ridiculous excess is to get the very last address in the database try: actionfunc(dc,lastoktown,lastokroad,lastokcivic,lastokstatus,lastoklon,lastoklat,opt) except Exception, errdet: print "> error < :",errdet print sys.exc_info()[0] def crawlItemAction(dc, town, road, civic, status, lon, lat, opt): pass def kmlItemAction(dc, town, road, civic, status, lon, lat, opt): """ handles an individual selected street address Called by processDB(), from kml(). Does not honour delay param. See processDB() for param description.""" if opt.kmlglobe: # absolutely nothing is done with this for now pass # placemark text block kmlbuffer.write(""" <Placemark> """+(opt.keepaddrs and [("<name>"+str(civic)+" "+road.replace("&","&")+", "+town+"""</name> """)] or [""])[0]+"""<styleUrl>#stylemap_"""+dslnames[(status+1)]+"""</styleUrl> <Point> <coordinates>"""+str(lon)+""","""+str(lat)+""",0</coordinates> </Point> </Placemark> """) # The only useful bit in this block is the <name> block. # However, this makes Google Earth rather cluttered, # adds to file size, and raises privacy concerns. # The '&' replacement is very important -- Earth chokes easily. # The <LookAt> block is useless bloat. # This stuff was originally above <styleUrl>. ## <name>"""+str(civic)+" "+road.replace("&","&")+", "+town+"""</name> ## <LookAt> ## <longitude>"""+str(lon)+"""</longitude> ## <latitude>"""+str(lat)+"""</latitude> ## <altitude>0</altitude> ## <range>600</range> ## <tilt>0</tilt> ## <heading>0</heading> ## <altitudeMode>relativeToGround</altitudeMode> ## <gx:altitudeMode>relativeToSeaFloor</gx:altitudeMode> ## </LookAt> # end of kmlItemAction() def statsItemAction(dc, town, road, civic, status, lon, lat, opt): """ handles an individual selected street address. Called by processDB(), from stats(). Does not honour delay param. For param details, see docstring for processDB().""" statistics[5] = statistics[5] + 1 # increment the total if status < 0: # error statistics[0] = statistics[0] + 1 elif status == 0: # unknown [EMPTY] statistics[1] = statistics[1] + 1 elif status == 1: # no [NODSL] statistics[2] = statistics[2] + 1 elif status == 2: # basic [BASIC] statistics[3] = statistics[3] + 1 elif status == 3: # ultra [ULTRA] statistics[4] = statistics[4] + 1 else: # error print "invalid status: ",civic, road,",", town,": ",status def listItemAction(dc, town, road, civic, status, lon, lat, opt): """ prints an individual selected street address. Called by fibmod.processDB, from printList(). Does not honour delay param. For param details, see docstring for fibmod.processDB().""" print dslnames[(status+1)]," ",civic,road+",",town def parseDSLMask(instr): """ parses four boolean values from string, or bool list. _Please_ don't use the less-than-four mechanism.""" dslmask = [ False, False, False, False ] # real dslmask for use dfl = len(instr) # input length if dfl >= 4: # 4 or more? only use first 4. for i in range(4): dslmask[i] = char2Bool(instr[i]) elif dfl == 3: # hopefully this won't be used -- EMPTY, NODSL, & (BASIC+ULTRA) for i in range(3): dslmask[i] = char2Bool(instr[i]) dslmask[3] = dslmask[2] elif dfl == 2: # hopefully to be ignored -- EMPTY, KNOWN dslmask[0] = char2Bool(instr[0]) dslmask[1] = dslmask[2] = dslmask[3] = char2Bool(instr[1]) elif dfl == 1: # hopefully to be ignored -- EMPTY [KNOWN = True] dslmask = [char2Bool(instr[0]), True, True, True] else: # easy -- all True dslmask = [ True, True, True, True ] return dslmask def char2Bool(inchar): """ Returns a bool from a y/n char param. y:Y: True; anything else: False However, if inchar is a bool, it will be returned as is. param sequence can be any length, but only the first char gets evaluated""" if inchar == True or inchar == False: return inchar elif inchar[0] == 'y' or inchar[0] == 'Y': return True else: return False def kmlMakeHeader(outkml, opt): """ Returns a header for a Google Earth KML file. Manipulates some of the styles based on the options. outkml: name of the KML file -- used as title opt: Miscellaneous options -- see docs elsewhere """ if opt.kmlglobe: # not implemented, but don't want it forgotten.... pass # determine the icon URLs to use iconURL = ["", "", "", "", ""] if opt.icontheme == "GM": iconURL = GMIconURLs else: for i in range(len(dslnames)-1): iconURL[i] = opt.icontheme.replace("*",dslnames[i]) # determine the icon hotspots iconHotspots = GMIconHotspot if opt.iconhotspot: ihsc = opt.iconhotspot.split(",") # chunk into x/y coords if len(ihsc) < 2: # not a coord print "KML icon hotspot invalid: Too few coords. Using default...." else: for i in range(2): iconHotspots[i] = int(ihsc[i]) # determine icon colors useIconColours = False iconColours = ["", "", "", "", ""] if opt.iconcolours: iconColours = opt.iconcolours.split(",") # chunk into icon colours if len(iconColours) < (len(dslnames)-1): # bad input print "KML icon colours invalid: Too few. Using default...." useIconColours = False else: useIconColours = True headerstr = """<?xml version="1.0" encoding="UTF-8"?> <kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom"> <Document> <name>"""+outkml+"""</name>""" for i in range(len(dslnames)-1): # don't want the last one -- it's TOTAL headerstr = headerstr+""" <StyleMap id="stylemap_"""+dslnames[i]+""""> <Pair> <key>normal</key> <styleUrl>#stylenormal_"""+dslnames[i]+"""</styleUrl> </Pair> <Pair> <key>highlight</key> <styleUrl>#style"""+((opt.nohighlight == True) and ["normal_"] or ["highlit_"])[0]+dslnames[i]+"""</styleUrl> </Pair>""" headerstr = headerstr+""" </StyleMap> <Style id="stylenormal_"""+dslnames[i]+""""> <IconStyle>"""+(useIconColours and [""" <color>"""+iconColours[i]+"""</color>"""] or [""])[0]+""" <scale>"""+str(opt.iconsize)+"""</scale> <Icon> <href>"""+iconURL[i]+"""</href> </Icon> <hotSpot x="""+"\""+str(iconHotspots[0])+"\" y=\""+str(iconHotspots[1])+"\""+""" xunits="pixels" yunits="pixels"/> </IconStyle> <ListStyle> </ListStyle> </Style>""" # this line was plagued by a mysterious bug, as was the other nohighlight line above. # This works -- please don't just change it for appearance if opt.nohighlight != True: headerstr = headerstr+""" <Style id="stylehighlit_"""+dslnames[i]+""""> <IconStyle>"""+(useIconColours and [""" <color>"""+iconColours[i]+"""</color>"""] or [""])[0]+""" <scale>"""+str(opt.iconsize*kmlIconHighlitScale)+"""</scale> <Icon> <href>"""+iconURL[i]+"""</href> </Icon> <hotSpot x="""+"\""+str(iconHotspots[0])+"\" y=\""+str(iconHotspots[1])+"\""+""" xunits="pixels" yunits="pixels"/> </IconStyle> <ListStyle> </ListStyle> </Style> """ headerstr = headerstr+""" <Folder> <name>"""+outkml+"""</name> <open>0</open> """ return headerstr # end of kmlMakeHeader() def kmlMakeFooter(outkml, opt): """ Returns a footer for a Google Earth KML file. Manipulates some of the styles based on the options. outkml: name of the KML file -- used as title opt: Miscellaneous options -- see docs elsewhere """ return """ </Folder> </Document> </kml> """ # end of kmlMakeFooter() if __name__ == "__main__": main()