"""
By Kirby Urner, 4D Solutions

Revised Jan 17:  'N' & 'W' swapped in cities dictionary (good eye Valerie Harvey)

A rich data structure:  

lists of lat/long tuples in a city indexed dictionary.  Distance function 
provided.  Could be used to introduce data structures in Python.  

Although spherical trig gets used, lessons needn't focus on that.

Typical exercise:  write getkms(citya, cityb) to return distance in
kilometers.

Typical usage:

>>> from gis import cities, getmiles
>>> pdx = cities['Portland, Ore.']
>>> miami = cities['Miami, Fla.']
>>> sea = cities['Seattle, Wash.']
>>> getmiles(pdx, sea)
146.20520973103621
>>> getmiles(pdx, miami)
2707.8603168909253

"""
from math import acos, cos, sin, radians
from xml.sax import make_parser
from xml.sax.handler import ContentHandler

cities = {
 'Albany, N.Y.': [(42, 40, 'N'), (73, 45, 'W')],
 'Albuquerque, N.M.': [(35, 5, 'N'), (106, 39, 'W')],
 'Amarillo, Tex.': [(35, 11, 'N'), (101, 50, 'W')],
 'Anchorage, Alaska': [(61, 13, 'N'), (149, 54, 'W')],
 'Atlanta, Ga.': [(33, 45, 'N'), (84, 23, 'W')],
 'Austin, Tex.': [(30, 16, 'N'), (97, 44, 'W')],
 'Baker, Ore.': [(44, 47, 'N'), (117, 50, 'W')],
 'Baltimore, Md.': [(39, 18, 'N'), (76, 38, 'W')],
 'Bangor, Maine': [(44, 48, 'N'), (68, 47, 'W')],
 'Birmingham, Ala.': [(33, 30, 'N'), (86, 50, 'W')],
 'Bismarck, N.D.': [(46, 48, 'N'), (100, 47, 'W')],
 'Boise, Idaho': [(43, 36, 'N'), (116, 13, 'W')],
 'Boston, Mass.': [(42, 21, 'N'), (71, 5, 'W')],
 'Buffalo, N.Y.': [(42, 55, 'N'), (78, 50, 'W')],
 'Calgary, Alba., Can.': [(51, 1, 'N'), (114, 1, 'W')],
 'Carlsbad, N.M.': [(32, 26, 'N'), (104, 15, 'W')],
 'Charleston, S.C.': [(32, 47, 'N'), (79, 56, 'W')],
 'Charleston, W. Va.': [(38, 21, 'N'), (81, 38, 'W')],
 'Charlotte, N.C.': [(35, 14, 'N'), (80, 50, 'W')],
 'Cheyenne, Wyo.': [(41, 9, 'N'), (104, 52, 'W')],
 'Chicago, Ill.': [(41, 50, 'N'), (87, 37, 'W')],
 'Cincinnati, Ohio': [(39, 8, 'N'), (84, 30, 'W')],
 'Cleveland, Ohio': [(41, 28, 'N'), (81, 37, 'W')],
 'Columbia, S.C.': [(34, 0, 'N'), (81, 2, 'W')],
 'Columbus, Ohio': [(40, 0, 'N'), (83, 1, 'W')],
 'Dallas, Tex.': [(32, 46, 'N'), (96, 46, 'W')],
 'Denver, Colo.': [(39, 45, 'N'), (105, 0, 'W')],
 'Des Moines, Iowa': [(41, 35, 'N'), (93, 37, 'W')],
 'Detroit, Mich.': [(42, 20, 'N'), (83, 3, 'W')],
 'Dubuque, Iowa': [(42, 31, 'N'), (90, 40, 'W')],
 'Duluth, Minn.': [(46, 49, 'N'), (92, 5, 'W')],
 'Eastport, Maine': [(44, 54, 'N'), (67, 0, 'W')],
 'Edmonton, Alb., Can.': [(53, 34, 'N'), (113, 28, 'W')],
 'El Centro, Calif.': [(32, 38, 'N'), (115, 33, 'W')],
 'El Paso, Tex.': [(31, 46, 'N'), (106, 29, 'W')],
 'Eugene, Ore.': [(44, 3, 'N'), (123, 5, 'W')],
 'Fargo, N.D.': [(46, 52, 'N'), (96, 48, 'W')],
 'Flagstaff, Ariz.': [(35, 13, 'N'), (111, 41, 'W')],
 'Fort Worth, Tex.': [(32, 43, 'N'), (97, 19, 'W')],
 'Fresno, Calif.': [(36, 44, 'N'), (119, 48, 'W')],
 'Grand Junction, Colo.': [(39, 5, 'N'), (108, 33, 'W')],
 'Grand Rapids, Mich.': [(42, 58, 'N'), (85, 40, 'W')],
 'Havre, Mont.': [(48, 33, 'N'), (109, 43, 'W')],
 'Helena, Mont.': [(46, 35, 'N'), (112, 2, 'W')],
 'Honolulu, Hawaii': [(21, 18, 'N'), (157, 50, 'W')],
 'Hot Springs, Ark.': [(34, 31, 'N'), (93, 3, 'W')],
 'Houston, Tex.': [(29, 45, 'N'), (95, 21, 'W')],
 'Idaho Falls, Idaho': [(43, 30, 'N'), (112, 1, 'W')],
 'Indianapolis, Ind.': [(39, 46, 'N'), (86, 10, 'W')],
 'Jackson, Miss.': [(32, 20, 'N'), (90, 12, 'W')],
 'Jacksonville, Fla.': [(30, 22, 'N'), (81, 40, 'W')],
 'Juneau, Alaska': [(58, 18, 'N'), (134, 24, 'W')],
 'Kansas City, Mo.': [(39, 6, 'N'), (94, 35, 'W')],
 'Key West, Fla.': [(24, 33, 'N'), (81, 48, 'W')],
 'Kingston, Ont., Can.': [(44, 15, 'N'), (76, 30, 'W')],
 'Klamath Falls, Ore.': [(42, 10, 'N'), (121, 44, 'W')],
 'Knoxville, Tenn.': [(35, 57, 'N'), (83, 56, 'W')],
 'Las Vegas, Nev.': [(36, 10, 'N'), (115, 12, 'W')],
 'Lewiston, Idaho': [(46, 24, 'N'), (117, 2, 'W')],
 'Lincoln, Neb.': [(40, 50, 'N'), (96, 40, 'W')],
 'London, Ont., Can.': [(43, 2, 'N'), (81, 34, 'W')],
 'Long Beach, Calif.': [(33, 46, 'N'), (118, 11, 'W')],
 'Los Angeles, Calif.': [(34, 3, 'N'), (118, 15, 'W')],
 'Louisville, Ky.': [(38, 15, 'N'), (85, 46, 'W')],
 'Manchester, N.H.': [(43, 0, 'N'), (71, 30, 'W')],
 'Memphis, Tenn.': [(35, 9, 'N'), (90, 3, 'W')],
 'Miami, Fla.': [(25, 46, 'N'), (80, 12, 'W')],
 'Milwaukee, Wis.': [(43, 2, 'N'), (87, 55, 'W')],
 'Minneapolis, Minn.': [(44, 59, 'N'), (93, 14, 'W')],
 'Mobile, Ala.': [(30, 42, 'N'), (88, 3, 'W')],
 'Montgomery, Ala.': [(32, 21, 'N'), (86, 18, 'W')],
 'Montpelier, Vt.': [(44, 15, 'N'), (72, 32, 'W')],
 'Montreal, Que., Can.': [(45, 30, 'N'), (73, 35, 'W')],
 'Moose Jaw, Sask., Can.': [(50, 37, 'N'), (105, 31, 'W')],
 'Nashville, Tenn.': [(36, 10, 'N'), (86, 47, 'W')],
 'Nelson, B.C., Can.': [(49, 30, 'N'), (117, 17, 'W')],
 'New Haven, Conn.': [(41, 19, 'N'), (72, 55, 'W')],
 'New Orleans, La.': [(29, 57, 'N'), (90, 4, 'W')],
 'New York, N.Y.': [(40, 47, 'N'), (73, 58, 'W')],
 'Newark, N.J.': [(40, 44, 'N'), (74, 10, 'W')],
 'Nome, Alaska': [(64, 25, 'N'), (165, 30, 'W')],
 'Oakland, Calif.': [(37, 48, 'N'), (122, 16, 'W')],
 'Oklahoma City, Okla.': [(35, 26, 'N'), (97, 28, 'W')],
 'Omaha, Neb.': [(41, 15, 'N'), (95, 56, 'W')],
 'Ottawa, Ont., Can.': [(45, 24, 'N'), (75, 43, 'W')],
 'Philadelphia, Pa.': [(39, 57, 'N'), (75, 10, 'W')],
 'Phoenix, Ariz.': [(33, 29, 'N'), (112, 4, 'W')],
 'Pierre, S.D.': [(44, 22, 'N'), (100, 21, 'W')],
 'Pittsburgh, Pa.': [(40, 27, 'N'), (79, 57, 'W')],
 'Portland, Maine': [(43, 40, 'N'), (70, 15, 'W')],
 'Portland, Ore.': [(45, 31, 'N'), (122, 41, 'W')],
 'Providence, R.I.': [(41, 50, 'N'), (71, 24, 'W')],
 'Quebec, Que., Can.': [(46, 49, 'N'), (71, 11, 'W')],
 'Raleigh, N.C.': [(35, 46, 'N'), (78, 39, 'W')],
 'Reno, Nev.': [(39, 30, 'N'), (119, 49, 'W')],
 'Richfield, Utah': [(38, 46, 'N'), (112, 5, 'W')],
 'Richmond, Va.': [(37, 33, 'N'), (77, 29, 'W')],
 'Roanoke, Va.': [(37, 17, 'N'), (79, 57, 'W')],
 'Sacramento, Calif.': [(38, 35, 'N'), (121, 30, 'W')],
 'Salt Lake City, Utah': [(40, 46, 'N'), (111, 54, 'W')],
 'San Antonio, Tex.': [(29, 23, 'N'), (98, 33, 'W')],
 'San Diego, Calif.': [(32, 42, 'N'), (117, 10, 'W')],
 'San Francisco, Calif.': [(37, 47, 'N'), (122, 26, 'W')],
 'San Jose, Calif.': [(37, 20, 'N'), (121, 53, 'W')],
 'San Juan, P.R.': [(18, 30, 'N'), (66, 10, 'W')],
 'Santa Fe, N.M.': [(35, 41, 'N'), (105, 57, 'W')],
 'Savannah, Ga.': [(32, 5, 'N'), (81, 5, 'W')],
 'Seattle, Wash.': [(47, 37, 'N'), (122, 20, 'W')],
 'Shreveport, La.': [(32, 28, 'N'), (93, 42, 'W')],
 'Sioux Falls, S.D.': [(43, 33, 'N'), (96, 44, 'W')],
 'Sitka, Alaska': [(57, 10, 'N'), (135, 15, 'W')],
 'Spokane, Wash.': [(47, 40, 'N'), (117, 26, 'W')],
 'Springfield, Ill.': [(39, 48, 'N'), (89, 38, 'W')],
 'Springfield, Mass.': [(42, 6, 'N'), (72, 34, 'W')],
 'Springfield, Mo.': [(37, 13, 'N'), (93, 17, 'W')],
 'St. John, N.B., Can.': [(45, 18, 'N'), (66, 10, 'W')],
 'St. Louis, Mo.': [(38, 35, 'N'), (90, 12, 'W')],
 'Syracuse, N.Y.': [(43, 2, 'N'), (76, 8, 'W')],
 'Tampa, Fla.': [(27, 57, 'N'), (82, 27, 'W')],
 'Toledo, Ohio': [(41, 39, 'N'), (83, 33, 'W')],
 'Toronto, Ont., Can.': [(43, 40, 'N'), (79, 24, 'W')],
 'Tulsa, Okla.': [(36, 9, 'N'), (95, 59, 'W')],
 'Vancouver, B.C., Can.': [(49, 13, 'N'), (123, 6, 'W')],
 'Victoria, B.C., Can.': [(48, 25, 'N'), (123, 21, 'W')],
 'Virginia Beach, Va.': [(36, 51, 'N'), (75, 58, 'W')],
 'Washington, D.C.': [(38, 53, 'N'), (77, 2, 'W')],
 'Wichita, Kan.': [(37, 43, 'N'), (97, 17, 'W')],
 'Wilmington, N.C.': [(34, 14, 'N'), (77, 57, 'W')],
 'Winnipeg, Man., Can.': [(49, 54, 'N'), (97, 7, 'W')]}


def rads(degs, mins, compass):
    r = radians(degs + (mins/60.0))
    if compass in ['S','W']:
        r = -r
    return r

def distance(lat1, lon1, lat2, lon2, r):
    return acos( cos(lat1)*cos(lon1) * cos(lat2)*cos(lon2) + \
                 cos(lat1)*sin(lon1) * cos(lat2)*sin(lon2) + \
                 sin(lat1)*sin(lat2) ) * r

def getmiles(citya, cityb):    
    lat1, lon1 = rads(*citya[0]), rads(*citya[1])
    lat2, lon2 = rads(*cityb[0]), rads(*cityb[1])    
    return distance(lat1, lon1, lat2, lon2, 3963.1)

thepath = "c:/documents and settings/kirby/My Documents/Projects/winterhaven/"

def makexml(thefile):
    f = open(thepath + thefile, 'w')
    f.write('<?xml version="1.0" encoding="ISO-8859-1"?>\n')
    f.write('<cities>\n')
    for city in cities:  # cities is a global in this context

        loc = tuple([s.strip() for s in city.split(',')])
        if len(loc)==3:
            f.write('<city name="%s" state="%s" country="%s">\n' % loc)
        else:
            f.write('<city name="%s" state="%s">\n' % loc)
        f.write('\t<lat  deg="%s" min="%s" dir="%s" />\n' % cities[city][0])
        f.write('\t<long deg="%s" min="%s" dir="%s" />\n' % cities[city][1])
        f.write('</city>\n')
    f.write('</cities>\n')
    f.close()

class CityHandler(ContentHandler):

    def __init__(self):
        self.thedict = {}
        self.key = ''
        self.latlong = []

    def startElement(self, name, attrs):
        if name=='city':
            key = "%s, %s" % (str(attrs.get('name')),
                              str(attrs.get('state')))
            country = str(attrs.get('country',''))
            if country<>'':
                key = key + ", " + country
            self.key = key
            
        if name in ['lat','long']:
            self.latlong.append(
                (int(attrs.get('deg')),
                 int(attrs.get('min')),
                 str(attrs.get('dir'))))
            if name=='long':
                self.thedict[self.key]=self.latlong
                self.latlong=[]
                self.key=''

def makecities(thefile):
    parser = make_parser()
    thehandler = CityHandler()
    parser.setContentHandler(thehandler)
    parser.parse(thepath + thefile)
    print thehandler.thedict
    
    
# code highlighted using py2html.py version 0.8