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.
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.
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.
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.
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).
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.
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.
#!/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}
}
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
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
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.