diff options
Diffstat (limited to 'ipl/gprogs/trkvu.icn')
-rw-r--r-- | ipl/gprogs/trkvu.icn | 695 |
1 files changed, 695 insertions, 0 deletions
diff --git a/ipl/gprogs/trkvu.icn b/ipl/gprogs/trkvu.icn new file mode 100644 index 0000000..9cd3c36 --- /dev/null +++ b/ipl/gprogs/trkvu.icn @@ -0,0 +1,695 @@ +############################################################################ +# +# File: trkvu.icn +# +# Subject: Program to display GPS track logs +# +# Authors: Gregg M. Townsend +# +# Date: October 1, 2005 +# +############################################################################ +# +# This file is in the public domain. +# +############################################################################ +# +# Trkvu displays GPS track logs, using color to indicate various +# characteristics such as velocity, direction, or time of day. +# +############################################################################ +# +# usage: trkvu file... +# +# Each file argument is a track log uploaded from a GPS receiver. +# Lines that end in three decimal values specify latitude, longutude, +# and altitude in that order. Lines with just two values omit the +# altitude. Lines without data indicate breaks between segments. +# +# Some colorings use timestamps from the track logs. A timestamp +# has the form "mm/dd/yyyy hh:mm:ss" or "yyyy/mm/dd hh:mm:ss" and +# precedes the latitude and longitude. +# +############################################################################ +# +# Track log colorings are selected by pressing a key: +# +# F color by File +# A color by Age +# O color by Orientation (direction of travel) +# V color by Velocity +# I color by Interval duration (GPS sample rate) +# S color Segments in contrasting colors +# Y color by time of Year +# D color by Day of week +# H color by Hour of day +# M color by Minute (repeating colors every 10 minutes) +# T color by Time of day +# +# Colorings can also be cycled: +# +# SP or CR cycle to next coloring +# BS or DEL cycle to preceding coloring +# +# A legend explains each coloring. If it shows individually labeled +# color blocks, the colors encode discrete values. If a spectrum +# is shown, the colors vary smoothly over a continuous range. +# +# Some colorings require timestamps. For these, tracks lacking +# timestamps are drawn in gray. +# +############################################################################ +# +# Zooming and Panning: +# +# To zoom to a particular region, sweep out the region using the +# left mouse button. To cancel a sweep, reduce its width or height +# to fewer than ten pixels. +# +# The window may be resized as desired. +# +# The following keyboard commands also affect the display region: +# +# + or = zoom in +# - or _ zoom out +# 0 or Home zoom to initial view +# arrow keys pan the display (hold Shift key for smaller pan) +# +############################################################################ +# +# Requires: Version 9 graphics +# +############################################################################ +# +# Links: datetime, graphics, mapnav, strings +# +############################################################################ + + +$include "keysyms.icn" + +link datetime +link graphics +link mapnav +link strings + +$define BORDER 10 # border widths + + +record view( # one view of data + cs, # cset of chars to select this view + ltitle, # legend title + hproc, # hue selection procedure + lproc) # legend procedure + +record point( # one point along a track + t, # time at point (real days & fraction since epoch) + x, y, # coordinates of point (longitude, latitude) + fhue) # hue assigned to original source file + + +global viewlist # list of views (view records) +global curview # current selected view + +global huelist # list of ColorValues of 180 hues + +global fnlist # file name list (for F legend) +global fhlist # file hue list (for F legend) + +global seglist # list of travel segments +global tmin, tmax # earliest and latest time seen +global xmin, xmax # westernmost and easternmost longitude seen +global ymin, ymax # northernmost and southernmost latitude seen + +global lbase # legend baseline y value +global lclip # clipping arguments for legend region +global mclip # clipping arguments for map region +global stdwin # std bg/fg window + + + +# ========================= Overall Control ========================= + +procedure main(args) + local e, v, xywh + + Window("size=800,800", "resize=on", "canvas=hidden", + "linewidth=2", "font=sans,bold,12", args) + stdwin := Clone("bg=white") + + viewlist := [ + # sequence here is followed by <SP> and <BS> + view('Ff', "File", byfile, flegend), + view('Aa', "Age", byage, agelegend), + view('Oo', "Orientation", orientation, olegend), + view('Vv', "Velocity", velocity, vlegend), + view('Ii', "Interval", byinterval, intlegend), + view('Ss', "Segments", segments, seglegend), + view('Yy', "time of Year", bymonth, monthlegend), + view('Dd', "Day", byday, daylegend), + view('Hh', "Hour", byhour, hourlegend), + view('Mm', "Minute", byminute, minutelegend), + view('Tt', "Time", bytime, timelegend), + ] + while /viewlist[-1] do pull(viewlist) + + seglist := [] # init data structures + fnlist := [] + fhlist := [] + + every load(!args) # load data + survey() # find extremes + fnlist := fnsimp(fnlist) # simplify filename list + + WAttrib("canvas=normal") # make display visible + hueinit() # init color manager + layout() # lay out display + mapinit(draw, , xmin, xmax, ymax, ymin, cos(dtor((ymin + ymax) / 2))) + + if *args > 1 then + Enqueue("f") # show initially by file + else if tmax > 0 then + Enqueue("a") # show initially by age + else + Enqueue("o") # show initially by orientation + + # ==================== main event loop ==================== + + while e := Event() do { + if upto((v := \!viewlist).cs, e) then { # if a view selector + curview := v + EraseArea() + mapgen() # regenerate map + } + else case e of { + !" \n\r": nextview(+1) # cycle view forward + !"\b\d": nextview(-1) # cycle view backward + &resize: { layout(); mapevent(e) } # resize window + default: { mapevent(e) } # possible standard action + } + } +end + +procedure nextview(d) # advance to next view in sequence + local i + + every i := 1 to *viewlist do + if curview === viewlist[i] then { + i := (i + *viewlist - 1 + d) % *viewlist + 1 + curview := viewlist[i] + mapgen() + return + } +end + + + +# ========================= Input ========================= + +procedure load(fname) # load data from one file + local f, h, p, w, t, x, y, a, line, ptlist + static n + initial n := 0 + + f := open(fname) | stop("cannot open ", fname) + h := huenum(n +:= 1) + put(fnlist, fname) + put(fhlist, h) + while line := read(f) do { + every put(w := [], words(line)) + if -90.0 <= numeric(w[-3]) <= 90.0 then + a := pull(w) # altitude + if x := numeric(w[-1]) & y := numeric(w[-2]) then { + t := tcrack(w[-4], w[-3]) | &null + /ptlist := [] + put(ptlist, p := point(t, x, y, h)) + } + else { + put(seglist, \ptlist) + ptlist := &null + next + } + } + + put(seglist, \ptlist) + close(f) + if /p then + write(&errout, " no data: ", fname) + return +end + +procedure tcrack(date, time) # translate date + time into real value + local day, sec + static smul + initial smul := 1.0 / (24 * 60 * 60) + + if date[3] == "/" then + date := map("CcYy/Mm/Dd", "Mm/Dd/CcYy", date) + if date == ("1989/12/31" | "1990/01/01") then + return &null + *time = 8 | fail + *date = 10 | fail + day := DateToSec(date) | fail + sec := ClockToSec(time) | fail + return smul * (day + sec) +end + +procedure survey() # survey data ranges + local p + + xmin := 180 + xmax := -180 + ymin := 90 + ymax := -90 + tmin := 100 * 365.25 + tmax := 0 + + every p := !!seglist do { + tmin >:= \p.t + tmax <:= \p.t + xmin >:= p.x + xmax <:= p.x + ymin >:= p.y + ymax <:= p.y + } + + if xmin > xmax then + stop(" nothing to display") # diagnostic already issued + + if tmin > tmax then + tmin := tmax := 0 + + return +end + +procedure fnsimp(fnlist) # simplify filename list + local f, i, j, s + + if *fnlist < 2 then fail + (coprefix ! fnlist) ? { + i := 1 + while i := upto('/') + 1 do + move(1) + } + (cosuffix ! fnlist) ? { + tab(upto('.') | 0) + j := -*tab(0) + } + f := [] + every put(f, (!fnlist)[i:j]) + return f +end + + + +# ========================= Color Management ========================= +# +# Map colors are taken from the fully saturated color spectrum, spaced +# every 2 degrees in HSV space. This yields 180 different colors, well +# within Icon's limit of 256. The greens are darkened a bit for better +# contrast with the white background; but the yellows are not, because +# a darkened yellow is really ugly. (For better contrast, some colorings +# use hue 55 instead of 60 for a yellow color.) + +procedure hueinit() # initialize hue table (360 entries) + local d, d2, v + + huelist := list(360) + every d := 0 to 359 do { + d2 := d - d % 2 # use 2-degree quanta + if 60 < d2 < 180 then # darken green region + v := integer(100 - 0.8 * (60 - abs(d2 - 120))) + else + v := 100 + huelist[d + 1] := HSVValue(d2 || "/100/" || v) + } + return +end + +procedure sethue(h) # set & cache color, given hue in degrees >= 0 + local k + static kprev + + if h := integer(h) % 360 then + k := huelist[h + 1] + else # use gray for invalid argument + k := "gray" + Fg(kprev ~===:= k) + return +end + +procedure huenum(n) # return hue from ordered list + static predef + initial predef := [240, 0, 120, 30, 180, 300, 50, 270, 70, 210, 330] + # blu red grn org cyan mgnta tan purp grn blu plum + + return predef[n] | (137 * n) % 360 +end + + + +# ========================= Map Drawing ========================= + +procedure layout() # configure window layout + local w, h, lh + + Bg("pale weak yellow") + Clip() + EraseArea() + Bg("white") + + w := WAttrib("width") + h := WAttrib("height") + + # set legend size and baseline + lh := 2 * BORDER + WAttrib("ascent") + lbase := BORDER + lh - BORDER + + # set legend clipping, and clear + lclip := [BORDER, BORDER, w - 2 * BORDER, lh] + Clip ! ([stdwin] ||| lclip) + Clip ! lclip + EraseArea() + + # set map clipping, and clear + mclip := [BORDER, lh + 2 * BORDER, w - 2 * BORDER, h - lh - 3 * BORDER] + Clip ! mclip + EraseArea() + + return +end + +procedure draw(win, pjn, a) # display map using curview + local ptlist, h, n, p, q, x1, y1, x2, y2, l + + Clip ! lclip + EraseArea() + GotoXY(2 * BORDER, lbase) + ltext(curview.ltitle) + ltext(": ") + curview.lproc() + + Clip ! mclip + every ptlist := !seglist do { + if *Pending() > 0 then break + p := &null + every q := !ptlist do { + l := project(pjn, [q.x, q.y]) + x2 := integer(get(l)) + y2 := integer(get(l)) + x2 <:= -32767 + y2 <:= -32767 + x2 >:= 32767 + y2 >:= 32767 + if \p then { + sethue(curview.hproc(p, q) | &null) + DrawLine(x1, y1, x2, y2) + } + else if *ptlist = 1 then { + sethue(curview.hproc(q, q) | &null) + FillRectangle(x2 - 1, y2 - 1, 3, 3) + } + p := q + x1 := x2 + y1 := y2 + } + } + return +end + + + +# ========================= Legend Writing ========================= +# +# Colors are written via &window, text in black via stdwin. + +procedure ltext(s) # write text + + return WWrites(stdwin, s) +end + +procedure lhue(h, t) # write hue block with optional caption + local x, w + + sethue(h) + x := WAttrib("x") + w := WAttrib("ascent") + FillRectangle(x, lbase + 1, w - 1, -w) + GotoXY(x + w, lbase) + ltext(\t) + return +end + +procedure lspectrum(h1, h2, n) # write spectrum of 6 colors from h1 to h2 + local i, m + + /n := 6 + m := (h2 - h1) / (n - 1.0) + every i := 1 to n do + lhue(h1 + m * (i - 1)) + return +end + + + +# ========================= View Procedures ========================= +# +# View procedures are paired: a legend procedure draws the legend and a +# hue selection procedure that chooses the hue for each segment. (Hue +# procedure return a value in degrees, or they fail, which draws gray.) + + +# F: color segments by source file, using colors set at load time + +procedure flegend() + local i + + every i := 1 to *fnlist do + lhue(fhlist[i], fnlist[i] || " ") + return +end + +procedure byfile(p, q) + return q.fhue +end + + +# A: color segments by age (relative to range of timestamps seen) + +procedure agelegend() + + ltext("oldest") + lspectrum(630, 360, 12) + ltext("newest") + return +end + +procedure byage(p, q) + + # purple oldest, green mid, red newest + return 630. - 270. * (\q.t - tmin) / (tmax - tmin) +end + + +# O: color segments by orientation (direction of travel) + +procedure olegend() + + ltext("N"); lspectrum(270, 180) + ltext("E"); lspectrum(180, 90) + ltext("S"); lspectrum(90, 0) + ltext("W"); lspectrum(360, 270) + ltext("N") + return +end + +procedure orientation(p, q) + + # blue north, teal east, olive south, red west + return 180. + rtod(atan(q.y - p.y, cos(dtor(q.y)) * (q.x - p.x))) +end + + +# V: color segments by velocity + +procedure vlegend() + + lhue(240, "1 ") + lhue(210, "2 ") + lhue(180, "3 ") + lhue(120, "4 ") + lhue( 55, "5 ") + lhue( 30, "6 ") + lhue( 0, "7 ") + lhue(300, "8 ") + lhue(270, "9 ") + ltext(" mph (x1, x10, ...)") + return +end + +procedure velocity(p, q) + local dt, dx, dy, d, mph + static hues + initial hues := [270, 240, 210, 180, 120, 55, 30, 0, 300, 270] + # 0 1 2 3 4 5 6 7 8 9 + # 10 20 30 40 50 60 70 80 90 + # 100 200 300 400 500 600 700 800 900 + + dt := 0 < (\q.t - \p.t) | fail + dx := cos(dtor(p.y)) * (q.x - p.x) + dy := q.y - p.y + d := sqrt(dx ^ 2 + dy ^ 2) + mph := integer(2.877 * d / dt + 0.5) + while mph > 9 do + mph /:= 10 + return hues[mph + 1] +end + + +# I: color segments by length of time interval + +procedure intlegend() + + lhue( 0, "0 ") + lhue( 30, "1 ") + lhue( 55, "2 ") + lhue(120, "4 ") + lhue(180, "8 ") + lhue(220, "16 ") + lhue(240, "32 ") + lhue(290, "64 sec") + return +end + +procedure byinterval(p, q) + local dt, i + static hues + initial hues := [0, 30, 55, 120, 180, 220, 240, 290] + # 0 1 2 4 8 16 32 64 + + dt := integer(86400. * (\q.t - \p.t) + 0.5) | fail + i := (2 + integer(log(0 < dt, 2))) | 1 + return hues[i | -1] +end + + +# S: emphasize individual segments in contrasting colors. + +procedure seglegend() + + lspectrum(137, 12*137, 12) + ltext("...") + return +end + +procedure segments(p, q) + static n + initial n := 0 + + return n +:= 137 +end + + +# Y: color segments by time of year as a spectrum + +procedure monthlegend() + + ltext("January") + lspectrum(525, 195, 12) + ltext("December") + return +end + +procedure bymonth(p, q) + + # cyan winter, green spring, red summer, blue fall + return 540. - (\q.t % 365.25) * (360. / 365.25) +end + + +# D: color segments by day of week + +procedure daylegend() + + lhue(240, "Sun ") + lhue(120, "Mon ") + lhue(165, "Tue ") + lhue( 55, "Wed ") + lhue( 30, "Thu ") + lhue(285, "Fri ") + lhue( 0, "Sat ") + return +end + +procedure byday(p, q) + static hues + initial hues := [240, 120, 165, 55, 30, 285, 0] + + return hues[1 + ((4 + integer(\q.t)) % 7)] +end + + +# H: color segments by hour in the day (0 to 11, repeated) + +procedure hourlegend() + + lhue(240, "12 ") + lhue(290, "1 ") + lhue(350, "2 ") + lhue( 30, "3 ") + lhue( 80, "4 ") + lhue(150, "5 ") + lhue(210, "6 ") + lhue(270, "7 ") + lhue(330, "8 ") + lhue( 55, "9 ") + lhue(120, "10 ") + lhue(180, "11 ") + return +end + +procedure byhour(p, q) + local h + static hues + initial hues := [240, 290, 350, 30, 80, 150, 210, 270, 330, 55, 120, 180] + + h := integer(24 * (\q.t - integer(q.t))) | fail + return hues[1 + h % 12] +end + + +# M: color segments by minute of the hour, mod 10 + +procedure minutelegend() + local i + + every i := 0 to 9 do + lhue(huenum(i + 1), ":x" || i || " ") + return +end + +procedure byminute(p, q) + local t + + t := 24 * 30 * (\p.t + \q.t) | fail # time in minutes since epoch + return huenum(1 + integer(t) % 10) +end + + +# T: color segments by a time-of-day spectrum + +procedure timelegend() + + ltext("midnight") + lspectrum(600, 420, 13) + ltext("noon") + lspectrum(420, 240, 13) + ltext("midnight") + return +end + +procedure bytime(p, q) + + # green morning, yellow noon, red afternoon, blue night + return 600. - 360. * (\q.t - integer(q.t)) +end |