xnearest: a map viewer in tcl/tk

From: narain@chetan.bellcore.com (Sanjai Narain)
Subject: Maps in Tcl/Tk
Message-ID: <D9EswI.9qv@walter.bellcore.com>
Organization: Morristown Research and Engineering
Date: Tue, 30 May 1995 20:57:54 GMT

     I would very much appreciate hearing about any work on
     doing maps in Tcl/Tk. For example, a map of a state 
     would show the main highways and cities. If you 
     zoom into a particular area, you would see local 
     streets. If you zoom in further you would see the 
     subdivisions (house plots) etc. 
Not quite what Sanjai asked about, but here's a tool I made a while back, when my office changed locations, and I didn't know where any of the nearby conference rooms or printers or whatnot were located.

table of contents

[home] [up] [previous]


top page throopw@sheol.org
Last edited Mon Jun 5 11:43:36 1995 - Last generated Mon Jun 5 11:43:44 1995

description

The xnearest tool is a tcl script used to display map images, with a rudimentary capability of finding objects "near" a selected map location.

running xnearest

The version of xnearest below can be run as a simple wish script. You'll have to supply maps images and lists of objects with map coordinates, and fill in the setting of the global tcl variable "lib" to point to these items. Examples of this are also given below.

But once suitable maps and objects are available, and any local #! twiddling done to find wish, xnearest is simply launched as a unix command. For example, you might say

    xnearest printer w1m1&
which asks xnearest to display the nearest printer to m1 on map w1.

types and coordinates

Each type of object (eg, "printer" in the example above) is represented by a list of map-coordinate/object-name pairs stored in a file. The coordinates follow a pre-existing coordinate system to which I adapted the tool, where the map name is encoded in the first two characters, and x,y location upon each map is encoded by a letter-digit pair. Thus all coordinates are 4 characters long, eg, w1m1 is map w1, row m, column 1, w2r2 is map w2, row r column 1.

In each map (images stored for this tool in .xbm form), either the alphabetic or the numeric coordinate can be up/down or left/right, and can increase in either direction along its axis. All this is regulated by config files.

Once running, the display shows a "here" flag in red text, on the map coordinate specified by the second argument, and the nearest object of the type named in the first argument, also in red text. In order not to clutter the map, only the nearest is shown by default, but multiple objects can be shown on the map, as outlined below. The intended use is for lots of objects, so showing specifically selected ones instead of all of them at once was a design goal.

buttons

Xnearest has only a few buttons to control its operation, but clicks on the map, on the list of objects, and edits to the current location, have various meanings.

other operations

selection

The select button treats the "location" field as a regular expression to filter out the list of objects displayed. For example, if you want to show only the objects exactly *at* a given location. But it can also be edited to contain (say) the name of the printer you are looking for, and then select will show you the information for that printer (if it is on the current map).

finding second nearest

By default, only the first object on the sorted list (that is, only the very nearest object) is marked on the map in red. If that isn't the one you want, you can use the left button on the mouse to click on any of the objects in the list to display their location.

[home] [up] [previous]


top page throopw@sheol.org
Last edited Mon Jun 5 11:43:36 1995 - Last generated Mon Jun 5 11:43:44 1995

implementation comments

The tool as it exists is a bit crude, but it has proven useful nevertheless.

Perhaps the clumsiest I felt implementing it was dealing with mapping from the odd coordinate notation I'd inherited to pixel offsets on the map. I have a nagging feeling that there's a simple way to do it that'd be much easier, but I haven't seen it yet (nor searched all that hard once I got a working version, as far as that goes).

However, I note that the scheme I came up with is sort of reminiscent (but more restricted than) the scheme used for html .map objects. In fact, an appropriate CGI tool could do all of this stuff for an html-ized display quite easily.

One of the things this tool doesn't do that the original news article talked about is the ability "zoom in" and "scale" and view from different perspectives and so on. If the maps were in line-drawing form instead of pixmaps, that could be done easily in tcl, using canvas scale operations and the such. This has been done in html as perl scripts, at xerox.

There are also efficiency issues; the tool rather promiscuously reloads maps, and doesn't do a good job of reusing the image if it already happens to be the right one. Nor does it do a good job of dealing with multiple map image sizes.

[home] [up] [previous]


top page throopw@sheol.org
Last edited Mon Jun 5 11:43:36 1995 - Last generated Mon Jun 5 11:43:44 1995

code

#!/usr/bin/env wish-f
#
# usage: xnearest type loc
#
# display the map that contains the given coordinate, and
# a text display of the nearest objects of the named type

proc nearest {t coord} {
    global lib map type loc text files
    foreach line [split [exec grep "^$map" $lib/$t.coord] \n] {
        lappend l [list [coord-order [lindex $line 0] $coord] $line]
    }
    foreach item [lsort $l] {
        append sl "[lindex $item 1]\n"
    }
    return $sl
}
proc coord-order {c1 c2} {
    scan $c1 %2s%c%1s c1map c1x c1y
    scan $c2 %2s%c%1s c2map c2x c2y
    if [string compare $c1map $c2map] \
        {error "coordinates $c1 and $c2 are on different maps"}
    set dx [expr $c1x-$c2x]
    set dy [expr $c1y-$c2y]
    return [format %08d [expr $dx*$dx+$dy*$dy]]
}

proc parameters {a} {
    global lib map type loc text files
    set lib /usr/local/tools/lib/nearest
    set type [lindex $a 0]
    set loc [lindex $a 1]
    scan $loc %2s map
    set text  [nearest [lindex $a 0] [lindex $a 1]]
    set files [split [exec ls $lib] \n]
}
parameters $argv

frame .panel
button .panel.quit -text quit -command {destroy .}
pack .panel.quit -side left -anchor nw
button .panel.redisplay -text redisplay \
       -command {redisplay loc [.panel.loc get 1.0 end]}
pack .panel.redisplay -side left -anchor nw
button .panel.select -text select -command { \
       dt .panel.closest \
           [exec grep -i [.panel.loc get 1.0 end] $lib/$type.coord]\n
       dt .panel.loc $loc }
pack .panel.select -side left -anchor nw
menubutton .panel.map  -menu .panel.map.menu -text map -relief raised
menu .panel.map.menu
foreach f $files {
    if [regexp "^(.*)\.map\.xbm$" $f dummy mapname] {
        .panel.map.menu add command -label $mapname \
                                     -command "redisplay loc [set mapname]a1"
    }
}
pack .panel.map -side left -anchor nw
menubutton .panel.type -menu .panel.type.menu -text type -relief raised
menu .panel.type.menu
foreach f $files {
    if [regexp "^(.*)\.coord$" $f dummy typename] {
        .panel.type.menu add command -label $typename \
                                     -command "redisplay type $typename"
    }
}
pack .panel.type -side left -anchor nw
text .panel.mapname -height 1 -width 15
pack .panel.mapname -side left -anchor nw
text .panel.typename -height 1 -width 15
pack .panel.typename -side left -anchor nw
text .panel.loc -height 1 -width 10
pack .panel.loc -side left -anchor nw
text .panel.closest -height 4 -width 30
pack .panel.closest -side left -anchor nw
pack .panel -side top -anchor nw
canvas .map -height 624 -width 900
pack .map -side top

proc dt {w t} {
    global lib map type loc text files
    $w delete 1.0 end
    $w insert 1.0 $t
}
proc dc {w m} {
    global lib map type loc text files
    foreach x [$w find all] {$w delete $x}
    $w create bitmap 0 0 -bitmap "@$lib/$m.map.xbm" -anchor nw
}
proc redisplay {replacevar replaceval} {
    global gridmap
    global lib map type loc text files
    set $replacevar $replaceval
    parameters [list $type $loc]
    set gridmap none
    catch {source "$lib/$map.config"}
    dt .panel.mapname   "  map: $map"
    dt .panel.typename  "type: $type"
    dt .panel.loc       $loc
    dt .panel.closest   $text
    dc .map $map
    mapmark $loc here
    if [regexp {^([^ ]*) +(.*)$} $text dummy firstloc firstdesc] {
        mapmark $firstloc [lindex [split $firstdesc \n] 0]
    }
}
# utilities for mapmark and mapsetloc
proc char2idx {char} {
    string first $char " abcdefghijklmnopqrstuvwxyz"
}
proc idx2char {idx} {
    string index " abcdefghijklmnopqrstuvwxyz" $idx
}
proc setxgrid {ns ne ps pe} {
    global gridmap
    global xppn xpstart xnstart
    if [regexp {[a-z]} $ns] {
        set ns [char2idx $ns]
        set ne [char2idx $ne]
        set gridmap xchar
    }
    set xppn [expr double($pe-$ps)/double($ne-$ns)]
    set xpstart $ps
    set xnstart $ns
}
proc setygrid {ns ne ps pe} {
    global gridmap
    global yppn ypstart ynstart
    if [regexp {[a-z]} $ns] {
        set ns [char2idx $ns]
        set ne [char2idx $ne]
        set gridmap ychar
    }
    set yppn [expr double($pe-$ps)/double($ne-$ns)]
    set ypstart $ps
    set ynstart $ns
}

# loc -> xy and xy -> loc routines...
proc mapmark {loc text} {
    global gridmap
    global xppn xpstart xnstart
    global yppn ypstart ynstart
    if {$gridmap!="none"} {
        if {$gridmap=="xchar"} {
            regexp {^..(.)(.*)$} $loc loc xn yn
            set xn [char2idx $xn]
            set xp [expr $xpstart+($xn-$xnstart)*$xppn]
            set yp [expr $ypstart+($yn-$ynstart)*$yppn]
        } {
            regexp {^..(.)(.*)$} $loc loc yn xn
            set yn [char2idx $yn]
            set xp [expr $xpstart+($xn-$xnstart)*$xppn]
            set yp [expr $ypstart+($yn-$ynstart)*$yppn]
        }
        if $xppn<0.0 {set xp [expr $xp+$xppn]}
        foreach x [.map find withtag $loc] {.map delete $x}
        .map create text $xp $yp -text $text -fill #f00 -anchor sw -tag $loc
    }
}
proc mapsetloc {x y} {
    global lib map type loc text files
    global xppn xpstart xnstart
    global yppn ypstart ynstart
    global gridmap
    if {$gridmap=="none"} {
        error "coordinate system undefined for this map"
    } {
        if {$gridmap=="xchar"} {
            set c [idx2char [expr int($xnstart+($x-$xpstart)/$xppn)]]
            set n [expr int($ynstart+($y-$ypstart)/$yppn)]
        } {
            set c [idx2char [expr int($ynstart+($y-$ypstart)/$yppn)]]
            set n [expr int($xnstart+($x-$xpstart)/$xppn)]
        }
        set loc "[set map][set c][set n]"
        dt .panel.loc $loc
        mapmark $loc here
    }
}

redisplay $map $map

bind .map <Button-1> {
    mapsetloc %x %y
    .panel.redisplay invoke
}
bind .panel.closest <Button-1> {
    regexp {^([^ ]+) +(.*)} \
        [.panel.closest get {@%x,%y linestart} {@%x,%y lineend}] p l t
    if [regexp "^$map" $l] {mapmark $l $t} {redisplay loc $l}
}

top page throopw@sheol.org
Last edited Mon Jun 5 11:43:36 1995 - Last generated Mon Jun 5 11:43:44 1995

example "coord" files

Each list of objects that the tool knows about is held in a simple file, with a coordinate followed by a description of the object. For example, the conference room coordinate file looks like this:
   tools/lib/nearest/cr.coord

    w1d3 Presentation
    w2c3 R&D 1a
    w2d3 R&D 1b
    w2b5 R&D 2
    w2o4 R&D 3
    w2n4 R&D 4
    w2s5 CNS 5
    w2r1 R&D 6
    w1m5 R&D 7
    w1l4 R&D 8

top page throopw@sheol.org
Last edited Mon Jun 5 11:43:36 1995 - Last generated Mon Jun 5 11:43:44 1995

example "config" files

Each map the tool knows about is held in an .xbm file (for the image itself), along with a .config file (to map pixel offsets to coordinate blocks. I simply used "xv" to find the pixel offsets of the grid lines on each image, and transcribed this information to the files below. Regular grid intervals are assumed, so pixel coordinates of only the lines corresponding to the starting and stopping map coordinates are given.
  tools/lib/nearest/k1.config
    setxgrid 1 18 842 130
    setygrid a e 376 187

  tools/lib/nearest/w1.config
    setxgrid 1 25 882 93
    setygrid a s 503 46

  tools/lib/nearest/w2.config
    setxgrid a s 91 811
    setygrid 1 7 450 132

top page throopw@sheol.org
Last edited Mon Jun 5 11:43:36 1995 - Last generated Mon Jun 5 11:43:44 1995

contents of "lib" directory

Consider this example "lib" directory contents:
       119 May 15 11:26 cr.coord
        43 Nov 23  1994 k1.config
    362032 Nov 22  1994 k1.map.xbm
      1015 May 15 11:24 printer.coord
        41 Nov 23  1994 w1.config
    362032 Nov 22  1994 w1.map.xbm
        41 Nov 23  1994 w2.config
    362032 Nov 22  1994 w2.map.xbm
It defines three maps (k1, w1 w2), and two types of objects to locate upon the maps (printers and "cr" (conference rooms)). Each list of objects is represented by a .coord file. Each map is represented by a .map.xbm file containing its bitmap, a .config file containing its coordinate to pixel offset mappings. (I scanned in the maps, then used xv to convert them to .xbm format from gif.)

As you can see, the .xbm format maps are huge. This problem can be addressed if desired by pbm extensions to tcl/tk, or perhaps by tk4.0 features. I wanted to be sure it would run in a vanilla wish/unix environment.


top page throopw@sheol.org
Last edited Mon Jun 5 11:43:36 1995 - Last generated Mon Jun 5 11:43:44 1995