Version 2 of TkTopoMap

Updated 2008-04-10 07:13:34 by WJP

Keith Vetter -- for another weekend project I needed to get some USGS topographical maps images. The USGS and Microsoft have combined to create TerraServer (see http://terraserver.homeadvisor.msn.com ) which is a vast, public domain, database of the USGS topo maps and aerial images in many different resolutions. The only complication in using TerraServer is that it works in UTM coordinates as opposed to latitude and longitude. Last weekend I finally got writing the script that does the conversion into UTM and grabs the images.

As a side project, I also wrote this little utility to share with others the core technology of getting maps off of TerraServer. This is just a very basic front-end GUI: you specify latitude and longitude (or UTM if you know it), the resolution and type of image and hit the "Get Map" button and it will display that image.

Notes about the images: each image is 200x200 pixels; the resolution is in meters per pixel; not all resolutions are available for all image types.

For grins, try looking at latitude 32.15112, longitude -110.83032, resolution 4m/p and as an image.

 #!/bin/sh
 # The next line is executed by /bin/sh, but not tcl \
     exec wish $0 ${1+"$@"}

 ##+##########################################################################
 #
 # TkTopoMap
 #
 # Simple front end to Microsoft's terraserver which servers up topo
 # maps and images. Has to convert from latitude and longitude into
 # Universal Transverse Mercator coordinates. The conversion is performed
 # using the WGS-84 datum.
 # by Keith Vetter
 #
 # Revisions:
 # KPV Aug 20, 2002 - initial revision
 #
 ##+##########################################################################
 #############################################################################

 package require Tk
 if {[catch {package require Img}]} {
     wm withdraw .
     tk_messageBox -icon error -title TkTopoMap \
         -message "TkTopoMap\n\nError: this program requires the Img package."
     exit
 }
 package require http 2.0

 proc Init {} {
     global env state 

     set state(latitude) ""                      ;# Value we'll map
     set state(longitude) ""                     ;# Value we'll map
     set state(utm) ""                           ;# utm of lat/long
     set state(vscale) "16 m/p"                  ;# Resolution of image
     set state(vtheme) Topo                      ;# Type of image
     set state(fetching) 0

     # Terraserver chunks images so we must adjust utm by this value
     array set state {
         mult,10  200    mult,11  400   mult,12  800   mult,13    1600
         mult,14  3200   mult,15  6400  mult,16  12800 mult,17  25600
         mult,18  51200  mult,19  102400
     }

     # Map resolutions available from the server
     set state(resolutions) [list "1 m/p" "2 m/p" "4 m/p" \
                                 "8 m/p" "16 m/p" "32 m/p" \
                                 "64 m/p" "128 m/p" \
                                 "256 m/p" "512 m/p"]
     set s 9
     foreach r $state(resolutions) {
         set state(res,$r) [incr s]              ;# String => scale
         set state(i,res,$s) $r                  ;# Scale => string
     }
     set state(themes) [list Topo Image]         ;# Type of images available
     set state(theme,Image) 1
     set state(theme,Topo)  2

     set state(url) "http://terraserver-usa.com/tile.ashx"
     append state(url) "?T=\$TTHEME&S=\$SSCALE&X=\$XX&Y=\$YY&Z=\$ZZONE&W=0"

     if {[info exists env(temp)]} {              ;# We put image into here
         set state(tempfile) [file join $env(temp) get_topo.temp]
     } elseif {[info exists env(tmp)]} {
         set state(tempfile) [file join $env(tmp) get_topo.temp]
     } elseif {[file isdirectory /tmp]} {
         set state(tempfile) [file join /tmp get_topo.temp]
     } else {
         set state(tempfile) get_topo.temp
     }
     trace variable state w DoTrace
 }
 ##+##########################################################################
 # 
 # Display
 # 
 # Sets up our simple dialog
 # 
 proc Display {} {
     global state

     wm title . TkTopoMap
     label .l

     frame .f0
     frame .f1a -relief sunken -bd 2
     frame .f1b -relief sunken -bd 2
     frame .f2 -relief sunken -bd 2
     frame .f2.a ; frame .f2.b
     frame .f3 -relief sunken -bd 2
     pack .f0 -side top -fill x -ipady 3 -ipadx 3
     pack .f3 .f2 -side bottom -fill x -ipady 3 -ipadx 3
     pack .f2.a .f2.b -side left -fill both -expand 1
     pack .f1a .f1b -side left -fill both -ipady 3 -ipadx 3

     label .title -text TkTopoMap \
         -font [eval font create [font actual [.l cget -font]] -size 18]
     label .title2 -text "by Keith Vetter" \
         -font [eval font create [font actual [.l cget -font]] -size 10]
     button .b -text "About TkTopoMap" -command about -font [.title2 cget -font]
     pack .title -in .f0 -side top
     pack .b -in .f0 -expand 1 -side left -pady 5


     label .latlong -text "Latitude/Longitude" \
         -font [eval font create [font actual [.l cget -font]] -size 10] 

     label .lat -text Latitude
     label .long -text Longitude
     entry .elat -width 15 -justify center -textvariable state(latitude)
     entry .elong -width 15 -justify center -textvariable state(longitude)
     label .spacer -text ""
     button .go1a -text " GetMap " -command GoLatLong
     grid .latlong - -in .f1a
     grid .lat .elat -sticky ew -in .f1a
     grid .long .elong -sticky ew -in .f1a
     grid .spacer - -sticky ew -in .f1a
     grid .go1a - -in .f1a -pady 10

     label .utm -text "UTM" \
         -font [eval font create [font actual [.l cget -font]] -size 10] 
     label .northing -text "Northing"
     entry .enorthing -width 15 -justify center -textvariable state(northing)
     label .easting -text Easting
     entry .eeasting -width 15 -justify center -textvariable state(easting)
     label .zone -text Zone
     entry .ezone -width 15 -justify center -textvariable state(zone)
     button .go1b -text " GetMap " -command GoUTM

     grid .utm - -in .f1b
     grid .northing .enorthing -sticky ew -in .f1b
     grid .easting .eeasting -sticky ew -in .f1b
     grid .zone .ezone -sticky ew -in .f1b
     grid .go1b - -in .f1b -pady 10

     label .sc  -text "Resolution"
     eval tk_optionMenu .esc state(vscale) $state(resolutions)
     .esc config -width 8
     label .theme -text Theme
     eval tk_optionMenu .etheme state(vtheme) $state(themes)
     .etheme config -width 8
     pack .sc .esc -side left -in .f2.a
     pack .theme .etheme -side left -in .f2.b

     label .terra -text "TerraServer:" -justify right
     label .eterra -textvariable state(terra) -width 30 -relief sunken
     canvas .c -bd 1 -relief ridge -width 200 -height 200
     .c create text 100 100 -anchor c -text "no image" -tag what
     image create photo map
     .c create image 0 0 -anchor nw -image map

     grid .terra .eterra -sticky ew -in .f3
     grid .c - -in .f3
 }
 ##+##########################################################################
 # 
 # DoTrace
 # 
 # Traces the state variable so we know when to enable certain buttons.
 # 
 proc DoTrace {arg1 arg2 op} {
     global state

     if {$state(fetching)} {                     ;# Always off while fetching
         .go1a config -state disabled
         .go1b config -state disabled
         return
     }

     set new disabled                            ;# Check the lat/long button
     if {$state(latitude) != "" && $state(longitude) != ""} {
         set new normal
     }
     .go1a config -state $new

     set new disabled                            ;# Check the utm button
     if {$state(northing) != "" && $state(easting) != "" && $state(zone) != ""} {
         set new normal
     }
     .go1b config -state $new
 }
 ##+##########################################################################
 # 
 # GoUTM
 # 
 # Handles getting map from UTM coordinates.
 # 
 proc GoUTM {} {
     global state
     set emsg ""

     if {! [string is double $state(northing)]} {
         set emsg "Can't figure out northing: '$state(northing)'"
     } elseif {! [string is double $state(easting)]} {
         set emsg "Can't figure out easting: '$state(easting)'"
     } elseif {! [string is double $state(zone)]} {
         set emsg "Can't figure out zone: '$state(zone)'"
     }
     if {$emsg != ""} {
         tk_messageBox -icon error -message $emsg
         return
     }
     set state(latitude) ""
     set state(longitude) ""
     GetMap
 }
 ##+##########################################################################
 # 
 # GoLatLong
 # 
 # Handles getting map from latitude and longitude.
 # 
 proc GoLatLong {} {
     global state

     # Extract the lat/long from the entry boxes
     set state(latitude) [string trim $state(latitude)]
     set lat $state(latitude)
     regsub -nocase {N *(.*)} $lat {\1} lat
     regsub -nocase {S *(.*)} $lat {-\1} lat
     if {[regexp {^[\d.]+$} $lat]} {
         ;
     } elseif {[regexp {^([\d.]+)\s+([\d.]+)\s+([\d.]+)$} $lat => a b c]} {
         set lat [expr {$a + $b/60.0 + $c/60.0/60.0}]
     } else {
         set emsg "Can't figure out $state(latitude)"
         tk_messageBox -icon error -message $emsg
         return
     }

     set state(longitude) [string trim $state(longitude)]
     set long $state(longitude)
     regsub -nocase {W *(.*)} $long {-\1} long
     regsub -nocase {E *(.*)} $long {\1} long
     if {[regexp {^-?[\d.]+$} $long]} {
         ;
     } elseif {[regexp {^(-?)([\d.]+)\s+([\d.]+)\s+([\d.]+)$} $long \
                    => west a b c]} {
         set long [expr {$a + $b/60.0 + $c/60.0/60.0}]
         if {$west != ""} { set long [expr {-1 * $long}]}
     } else {
         set emsg "Can't figure out $state(longitude)"
         tk_messageBox -icon error -message $emsg
         return
     }

     # Convert first to UTM coordinates
     set state(utm) [ll2utm $lat $long]
     foreach {state(northing) state(easting) state(zone)} $state(utm) break

     GetMap
 }
 ##+##########################################################################
 # 
 # GetMap
 # 
 # Fetches and displays the map for this utm from the terraserver. We
 # fetch the image straight into a temp file for efficiency.
 # 
 proc GetMap {} {
     global state

     # Adjust scale for this image's theme
     set state(scale) $state(res,$state(vscale))
     set state(theme) $state(theme,$state(vtheme))
     if {$state(theme) == 2 && $state(scale) == 10} { set state(scale) 11 }
     if {$state(theme) == 1 && $state(scale) >= 17} { set state(scale) 16 }
     set state(vscale) $state(i,res,$state(scale))

     catch {image delete map}
     .c itemconfig what -text "fetching image"
     set state(fetching) 1

     # Chunk utm for
     set mult $state(mult,$state(scale))
     set XX [expr {int($state(easting)  / $mult)}]
     set YY [expr {int($state(northing) / $mult)}]
     set SSCALE $state(scale)
     set TTHEME $state(theme)
     set ZZONE $state(zone)
     set state(xurl) [subst $state(url)]
     set state(terra) "X=$XX  Y=$YY  Z=$ZZONE  MULT=$mult"
     after 1 GetMap2
 }
 ##+##########################################################################
 # 
 # GetMap2
 # 
 # This gets the actual map. It's a separate procedure so we can call it
 # as an after task.
 # 
 proc GetMap2 {} {    
     global state
     ;# Fetch the image from terraserver into our tempfile
     set f [open $state(tempfile) w]
     set token [::http::geturl $state(xurl) -channel $f]
     ::http::wait $token
     close $f

     ;# Display the image
     image create photo map -file $state(tempfile)
     file delete $state(tempfile)
     set state(fetching) 0
 }
 ##+##########################################################################
 # 
 # ll2utm
 # 
 # Convert latitude and longitude into Universal Transverse Mercator (UTM)
 # coordinates. Lots of fun math which I got off the web.
 # 
 proc ll2utm {latitude longitude} {
     set PI [expr {atan(1) * 4}]
     set K0 0.9996

     # WGS-84
     set er 6378137                              ;# EquatorialRadius
     set es2 0.00669438                          ;# EccentricitySquared
     set es4 [expr {$es2 * $es2}]
     set es6 [expr {$es2 * $es2 * $es2}]

     # Must be in the range -180 <= long < 180
     while {$longitude < -180} { set longitude [expr {$longitude + 360}]}
     while {$longitude >= 180}  { set longitude [expr {$longitude - 360}]}

     # Now convert
     set lat_rad [expr {$latitude * $PI / 180.0}]
     set long_rad [expr {$longitude * $PI / 180.0}]

     set zone [expr {int(($longitude + 180) / 6) + 1}]
     if {$latitude >= 56.0 && $latitude < 64.0 &&
         $longitude >= 3.0 && $longitude < 12.0} {
         $zone = 32
     }
     if { $latitude >= 72.0 && $latitude < 84.0 } {
         if { $longitude >= 0.0  && $longitude <  9.0 } {$zone = 31;}
         if { $longitude >= 9.0  && $longitude < 21.0 } {$zone = 33;}
         if { $longitude >= 21.0 && $longitude < 33.0 } {$zone = 35;}
         if { $longitude >= 33.0 && $longitude < 42.0 } {$zone = 37;}
     }
     # +3 puts origin in middle of zone    
     set long_origin [expr {( $zone - 1 ) * 6 - 180 + 3}]
     set long_origin_rad [expr {$long_origin * $PI / 180.0}]
     set eccPrimeSquared [expr {$es2 / ( 1.0 - $es2 )}]
     set N [expr {$er / sqrt( 1.0 - $es2 * sin( $lat_rad ) * sin( $lat_rad ) )}]
     set T [expr {tan( $lat_rad ) * tan( $lat_rad )}]
     set C [expr {$eccPrimeSquared * cos( $lat_rad ) * cos( $lat_rad )}]
     set A [expr {cos( $lat_rad ) * ( $long_rad - $long_origin_rad )}]
     set M [expr { $er * ( \
        (1.0 - $es2 / 4 - 3 * $es4 / 64 - 5 * $es6 / 256) * $lat_rad \
        - (3 * $es2 / 8 + 3 * $es4 / 32 + 45 * $es6 / 1024) * sin(2 * $lat_rad) \
        + (15 * $es4 / 256 + 45 * $es6 / 1024 )             * sin(4 * $lat_rad) \
        - (35 * $es6 / 3072 )                               * sin(6 * $lat_rad) \
                               )}]
     set easting [expr {$K0 * $N * ( $A + ( 1 - $T + $C ) * $A * $A * $A / 6 \
        + ( 5 - 18 * $T + $T * $T + 72 * $C - 58 * $eccPrimeSquared ) * \
        $A * $A * $A * $A * $A / 120 ) + 500000.0}]
     set northing [expr {$K0 * ( $M + $N * tan( $lat_rad ) * \
        ( $A * $A / 2 + ( 5 - $T + 9 * $C + 4 * $C * $C ) * \
        $A * $A * $A * $A / 24 + ( 61 - 58 * $T + $T * $T + \
        600 * $C - 330 * $eccPrimeSquared ) * \
        $A * $A * $A * $A * $A * $A / 720 ) )}]

     if {$latitude < 0} {  ;# 1e7 meter offset for southern hemisphere  
         set northing [expr {$northing + 10000000.0}]
     }

     set northing [expr {int($northing)}]
     set easting [expr {int($easting)}]
     return [list $northing $easting $zone]
 }
 ##+##########################################################################
 # 
 # about
 # 
 # Your basic about box.
 # 
 proc about {} {
     set msg ""
     append msg "TkTopoMap\nby Keith Vetter\n\n"
     append msg "TkTopoMap is a simple front end for TerraServer, Microsoft's\n"
     append msg "database of USGS aerial images and topographic maps.\n"
     append msg "This data is free to everyone.\n\n"

     append msg "Fetching images from TerraServer is straightforward except\n"
     append msg "for the conversion from latitude and longitude into\n"
     append msg "Universal Transverse Mercator (UTM) coordinates. The\n"
     append msg "conversion is done using the WGS-84 datum.\n\n"
     append msg "For more details see http://terraserver.homeadvisor.msn.com."

     tk_messageBox -message $msg -title TkTopoMap

 }
 ################################################################
 ################################################################
 ################################################################

 Init
 Display

 set state(latitude) 40.069
 set state(longitude) -82.517 
 set state(latitude) "37 48 9"
 set state(longitude) "-122 13 29"

 if {0} {                                        ;# Beale Airforce base
     set state(latitude) 32.15112
     set state(longitude) -110.83032
     set state(vscale) "4 m/p"   
     set state(vtheme) Image
 }