|
25 Years of Programming
An open source source for C, C++, OWL, BASIC, MDB, XLS, DOT, and more... |
Home Projects Up Sitemap Search Blog Forum+Chat About Us Privacy Terms of Use Feedback FAQ Images Services Ads Donate Humor |
|
|
Amateur Radio Net Control programs - C++ versionsThis is the source code listing for the C++ programs in the Amateur Radio Net Control project, utilities that assist an amateur radio net control operator with running and checking members into an on the air radio net. |
|
/* NET.CPP 2-22-96
Copyright (C)1994-1996 Steven Whitney.
Initially published by http://25yearsofprogramming.com.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License (GPL)
Version 2 as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
For conducting amateur radio nets. Provides a fast interface for checking people in
individually by call sign or by roll call by sequential city names.
Allows recording announcements and traffic. At the end, calculates check-in and traffic
totals and generates a log of the session. Also allows printing a roster, which I used
to print on graph paper, for use during mobile operation away from the computer.
It will take several sessions to get used to the program's interface, but once you know how
to "drive" the program, it is fast, and you can keep up with checkins even if they
come in at a fast pace.
All data files must be in the current default directory (with the program).
------
TO DO:
(copied from paper notes, 6/24/02)
--allow reading a log file for review or for after a crash? Must write it first.
--if requestee of contact is in same city as requestor, update requestee's color also.
you do need to limit the length of the strings output in displayrec(), using
string::substr(). If a too-long name is the last on the line, it wraps to
the next line, overwriting part or all of the 1st record on the next line.
This may not be that important: it would have to be on line 25 to scroll
the screen, which never happens.
in callrollcall(), bycity(), etc., break the absolute numbers that calculate screen
coordinates out into named variables, and use those so method is more clear,
and can possibly be generalized. Put the generalized routine into a function.
calcfieldloc(int& x, int& y). Would be useful for displaying any similar
array where user needs to choose one from those displayed on screen.
Continue to watch for other situations where erroneous input causes serious problems,
and for places where intuitive commands aren't correct (use them, instead).
Note that A,L,P checkin codes ARE commands in main menu context.
If someone has multiple traffic or traffic + announcements,
traffic count report is wrong (each call can only be credited with 1, and pgm can
only keep track of 1 per call).
also, traffic is counted even if it's not passed during the net.
there should be a way to check it off if it's passed.
Could tally using the cities list (where it's counted).
When main menu page space runs out, move "utilities" section to its own menu.
Write better documentation with an index and table of contents.
(a search by name might be interesting, but not particularly useful in real use)
--A Windows version would require significant mastery of a number of Windows methods;
would be worth it, especially because it could use DDE to maintain its database.
--Would also be interesting and instructive to attempt converting (rewriting)
in MSAccess, using macros and modules for some of the commands.
See NET.MDB: it's interesting to work on, but the chances of achieving the same ease of use
as this seem to be about zero.
--As an experiment, could store command key mappings in a file, making them user-definable.
--When extendedlog is on, if each report from a station is logged as an
announcement, it will automatically log the time, call, and what they were reporting.
And as people sign back OUT, you use ' ' to check them back out, until no one left.
--delete a record by setting ->ck to Visitor and doing an edit to force disk write.
Don't make it easier. Preferred method is to review with text editor.
--some searches could quit when past where item should be, but trivial savings.
*/
#include <classlib\time.h>
#include <classlib\arrays.h>
#pragma hdrstop
#include "c:\bcs\my.h"
#include "c:\bcs\mylib.cpp"
//////////////////////////////////////////////////////////////////////////////
// GLOBAL DATA
//////////////////////////////////////////////////////////////////////////////
// a city name and a count of the traffic items addressed to it
struct TOWN
{
TOWN() { tcount = 0; } // constructors
TOWN(string& a) : n(a) { n.to_upper(); tcount = 0; }
BOOL operator == (const TOWN& other) const { return(n == other.n); }
BOOL operator < (const TOWN& other) const { return(n < other.n); }
string n; // name of the city (was char[16])
int tcount; // number of pieces of TRAFFIC destined FOR THE CITY
};
//////////////////////////////////////////////////////////////////////////////
// info about one ham radio operator
struct HAM
{
HAM() { ck = need = ' '; ord = 0; sort = bycall; } // default
HAM(string& a, string& b, TOWN* c); // normally-used
HAM(const HAM& h) // copy constructor (not used)
{ call = h.call; name = h.name; city = h.city;
ck = h.ck; need = h.need; ord = h.ord; }
BOOL operator == (const HAM& other) const { return(call == other.call); }
BOOL operator < (const HAM& other) const;
HAM& operator = (const HAM& h); // ham1 = ham2 is legal (not used)
static enum sorttypes { bycall, bycitycall, byord } sort;
string call; // call sign
string name; // person's name, only 9 chars are shown on displays
TOWN* city; // home city
char ck; // CHECKIN CHAR
char need; // IF NEEDED FOR CONTACT, matches the other party's char
int ord; // PLACEMENT IN CHECKIN ORDER (BUT doesn't count accurately)
};
HAM::sorttypes HAM::sort = HAM::bycall;
//----------------------------------------------------------------------------
// allows sorting in multiple ways, depending on the sort flag.
// Whenever you create a new HAM, sort order is reset to bycall.
// After that, you can change it for other sorts in a new sorted container.
BOOL HAM::operator < (const HAM& other) const
{
switch(sort)
{
case bycitycall:
if(city == other.city)
return(call < other.call);
return(city->n < other.city->n);
case byord :
return(ord < other.ord);
case bycall:
default:
return(call < other.call);
}
}
//----------------------------------------------------------------------------
// constructor
HAM::HAM(string& a,string& b,TOWN* c) : call(a), name(b), city(c)
{
call.to_upper();
name.to_lower();
name[0] = toupper(name[0]); // 1st letter of name capitalized
ck = need = ' ';
ord = 0;
sort = bycall;
}
//----------------------------------------------------------------------------
// assignment operator (adapted from PG:151).
// Note (*this) is the object being assigned TO.
HAM& HAM::operator = (const HAM& h)
{
if(&h == this) return(*this); // prevent assigning to itself
call = h.call;
name = h.name;
city = h.city;
ck = h.ck;
need = h.need;
ord = h.ord;
return(*this);
}
//----------------------------------------------------------------------------
// MORE GLOBAL DATA
TISArrayAsVector<HAM> db(200,0,50); // sorted database of all people
TISArrayAsVector<HAM> fi(20,0,50); // subset of db[] found during searches
TArrayAsVector<string> memos(3,0,5); // ARRAY OF SINGLE-LINE MEMOS
TArrayAsVector<string> agenda(60,0,5); // lines from agenda file
TISArrayAsVector<TOWN> cities(50,0,10); // list of all the cities on file
int cindex = 0; // index within cities[] of CURRENTLY SELECTED CITY
int resumecity = 0; // CITY TO RESUME INPUT WITH AFTER PAGING BACKWARDS
BOOL filteron = FALSE; // WHETHER to hide already-checked-in from displays
BOOL dbchanged = FALSE; // WHETHER TO write file calls.dat BEFORE EXIT
BOOL verifycalls = TRUE; // whether to check call signs for US, Canada validity
BOOL extendedlog = FALSE; // whether to log every net activity with time stamp.
// The extended log is actually kept in the .MEM file
int ckinord = 0; // ORDERS CHECKINS, BUT DOESN'T KEEP ACCURATE COUNT.
char contacts = '0'; // 2 parties of a contact have a matching contact char
char defch = 'x'; // DEFAULT CHECKIN CHARACTER (ONLY LOPX)
constream scr; // window for screen output
string helpline; // each function sets it to its relevant help line
//----------------------------------------------------------------------------
// Input a string from the console (maximum length 80), using ::cgets() so that
// the user's ending <CR> won't cause screen to scroll.
// (If length > 80 is anticipated, use getline(), since scrolling will occur anyway.)
// Could be a library function.
// Returns the length of the resulting string. Can use if(cgets(s)) dosomething.
int cgets(string& s, int maxlen = 80)
{
if(maxlen < 1) maxlen = 1; // entering 0 chars is meaningless
if(maxlen > 80) maxlen = 80; // > 80 will cause scrolling AND buf overflow
char buf[82] = {maxlen}, *p; // room for 79 chars + null (max w/o scrolling)
p = cgets(buf);
s = p;
return(s.length());
}
//----------------------------------------------------------------------------
// sets a string to today's date, reformatted to "mm-dd-yy" (useful for file names)
// Could be a library function.
// could this return a string&? and thus be chained like other operators?
void today(string& s)
{
char buf[10];
_strdate(buf); // GET DOS DATE
for(char* p = buf ; *p ; p++) // change slashes to hyphens
if(*p == '/')
*p = '-';
s = buf; // set string
}
//----------------------------------------------------------------------------
// sets a string to the current time
// Could be a library function.
void timenow(string& s)
{
char buf[10];
_strtime(buf); // GET DOS DATE
s = buf; // set string
}
//----------------------------------------------------------------------------
// check whether a given call sign is probably a valid one
BOOL validcallsign(string& s)
{
if(!verifycalls) // no checking: anything is ok
return(TRUE);
if((s.length() < 4) || (s.length() > 6)) // length: 4 to 6 chars
return(FALSE);
if(!strchr("acknvwx",tolower(s[0]))) // start with AKNW(CVX Canada X Mex)
return(FALSE);
if(!isdigit(s[1]) && !isdigit(s[2])) // digit in position 2 or 3
return(FALSE);
return(TRUE);
}
//----------------------------------------------------------------------------
// create a memo, optionally getting additional text from user
// Assumes that if there IS starttext, and autoret is FALSE, it's an announcement;
// if no text is added to it, considers it an abort.
// returns TRUE if a memo was created and added, FALSE if not
BOOL memo(string& starttext, BOOL autoret) // initial text & auto-exit flag
{
static string previousdate;
string memotext, timestamp;
if(extendedlog) // create date and time stamp
{
today(timestamp);
if(timestamp != previousdate) // if date has changed since last memo
{ // FIRST make them equal, to prevent
previousdate = timestamp; // endless recursive calls!
memo(timestamp,TRUE); // a memo to note that the date changed
}
timenow(timestamp); // now get current time to prefix memo
timestamp += ": ";
}
memotext = starttext; // START MEMO WITH imported STRING
if(!autoret) // GET ADDITIONAL USER TEXT
{
scr << setclr(WHITE) << setxy(1,25) << clreol << setcrsrtype(_SOLIDCURSOR)
<< memotext;
string userbuf;
cgets(userbuf,80-memotext.length()); // get additional text, if any, from user
scr << setcrsrtype(_NORMALCURSOR);
if(memotext.length() && !userbuf.length()) // user aborted announcement
return(FALSE);
memotext += userbuf; // append it to the string
}
if(memotext.length()) // IF MEMO has anything in it,
{
if(extendedlog)
memotext.prepend(timestamp); // insert the time stamp
memos.Add(memotext); // add it to the array.
return(TRUE);
}
return(FALSE);
}
//----------------------------------------------------------------------------
// DISPLAYS all MEMOS
// RETURNS: int, WHICH CAN BE A MAIN MENU COMMAND
int showmemos()
{
scr.clrscr();
scr << setclr(LIGHTGRAY);
for(int i = 0 ; i < memos.GetItemsInContainer() ; i++)
{
scr << memos[i] << endl;
if(i && !(i % 23))
{
scr << setxy(1,25) << clreol << "More...";
getch();
scr << endl;
}
}
scr << setclr(GREEN) << setxy(1,25) << clreol << "Enter command or <CR> for menu: ";
return(ci());
} // showmemos
//----------------------------------------------------------------------------
// WRITES MEMOS TO DISK FILE MM-DD-YY.MEM
void writememos()
{
string buf;
today(buf); // get today's date
buf += ".mem"; // ADD EXTENSION
ofstream outfile(buf.c_str());
for(int i = 0 ; i < memos.GetItemsInContainer() ; i++)
outfile << memos[i] << endl;
if(outfile.fail()) // file write error
{
cerr << "Possible data loss in file " << to_upper(buf) << endl;
presskey();
}
} // writememos
//----------------------------------------------------------------------------
// search db for a call sign
// returns pointer to a HAM node, or 0 if call sign not found
HAM* findham(string& calltofind) // call sign to look for
{
calltofind.to_upper();
for(int i = 0 ; i < db.GetItemsInContainer() ; i++) // LOOK FOR Call sign
if(db[i]->call == calltofind) // IF FOUND,
return(db[i]); // return pointer.
return(0); // wasn't found
} // findham
//----------------------------------------------------------------------------
// PROCESS CONTACT REQUEST.
// ASSIGNS NEXT CONTACT REQUEST CHAR TO THE NEED FIELD OF BOTH PARTIES.
// IF NEEDED PARTY NOT FOUND, CONTACT IS NOTED FOR REQUESTOR,
// AND A MEMO IS ADDED TO MEMO PAGE, BUT NEEDED PARTY NOT ADDED TO DB
// RETURNS: TRUE IF a contact request was processed, FALSE IF NOT (user aborted)
BOOL listneed(HAM* req) // REQUESTING PARTY
{
string buf, memostart;
scr << setclr(WHITE) << setxy(1,25) << clreol
<< "Contact with which call? <CR> to quit: ";
if(!cgets(buf)) // ALLOW ABORT
return(FALSE);
HAM* wanted = findham(buf);
if(wanted) // if wanted party is in db,
{
wanted->need = contacts; // MARK as wanted for contact
if(extendedlog) // if logging everything, log the contact request
{
memostart = "Contact request: " + req->call + " for " + buf;
memo(memostart,TRUE);
}
}
else // otherwise, make a memo.
{
memostart = "Contact request: " + req->call + " for " + buf
+ " (who is not in database).";
memo(memostart,TRUE); // NOTE that WANTED CONTACT party wasn't IN DB
}
req->need = contacts++; // MARK REQUESTOR and increment the marker char
return(TRUE);
} // listneed
//----------------------------------------------------------------------------
// return pointer to a city node, first creating the node if not already listed
TOWN* findcity(string& tofind) // name of city to look for
{
size_t u; // change any imbedded commas to spaces
while((u = tofind.find(",")) != NPOS) // (commas not allowed by disk file format)
tofind[u] = ' ';
tofind.to_upper();
for(int i = 0 ; i < cities.GetItemsInContainer() ; i++) // LOOK FOR CITY NAME
if(cities[i]->n == tofind) // IF FOUND,
return(cities[i]); // return pointer.
string cindexcity(tofind); // If 1st city, this WILL be cities[cindex]->n
if(cities.GetItemsInContainer()) // else (if there IS a current cindex city),
cindexcity = cities[cindex]->n; // save its name. The new addition
// might change its index within the array.)
TOWN* newcity = new TOWN(tofind); // tofind wasn't FOUND above, so create IT,
cities.Add(newcity); // add to list,
if(cities[cindex]->n != cindexcity) // if [cindex] no longer is same city
for(i = 0 ; i < cities.GetItemsInContainer() ; i++)
if(cities[i]->n == cindexcity) // find that city
cindex = i; // and point [cindex] back at it.
return(newcity); // return pointer to the added city
} // findcity
//----------------------------------------------------------------------------
// INCREMENT TRAFFIC COUNT FOR A CITY. Can be any city name. If not in roster
// (no ham living there), will be temporarily added to cities list as a reminder.
// returns TRUE if a traffic item was added, FALSE if not.
BOOL logtraf(HAM* from) // STATION WITH TRAFFIC
{
// as an alternative, could use the citymenu() function to select one from the list.
// but it would still need this option of creating a new city.
scr << setclr(WHITE) << setxy(1,25) << clreol
<< "Destination city or <CR> to quit: ";
string destcity;
if(!cgets(destcity)) // allow ABANDON ENTRY
return(FALSE);
// look up (or create) city
findcity(destcity)->tcount++; // and increment traffic count
string memostart = "T: " + from->call + " for " + destcity; // memo for reference
memo(memostart,TRUE);
return(TRUE);
} // logtraf
//----------------------------------------------------------------------------
// PROCESS CHECKIN CHARS T, A, C
// returns TRUE if any of its activities might change screen attributes
// (different color, contact request, etc.) of a displayed db record OTHER
// than the one being processed, OR change a city's traffic count, FALSE if not.
// If the entering of t,a, or c is aborted, DON'T change the ham's ->ck checkin
// code: they might have had others; and if you allow ->ck to be auto-changed
// here, you can't restore their previous char without entering new info,
// thus creating an extraneous log entry (and count).
// When extendedlog is ON, all three (tac) cause memos to be created.
BOOL handletac(HAM* h) // pointer to node of CALL SIGN BEING PROCESSED
{
string memostart;
switch(h->ck) // checkin char must have been assigned before entering
{
case 't': // RECORD PRESENCE OF TRAFFIC
return(logtraf(h));
case 'a': // RECORD ANNOUNCEMENT
memostart = "A: " + h->call + " ";
memo(memostart,FALSE);
return(FALSE); // affects no other record
case 'c': // PROCESS CONTACT REQUEST
return(listneed(h)); // contact could affect others on same page
}
return(FALSE); // any non-tac char affects nothing
} // handletac
//----------------------------------------------------------------------------
// user ADDs NEW PERSON TO DATABASE.
// default city is cities[cindex] (currently selected city)
// RETURNS: 0=successfully added, 1=user aborted, 2=invalid call, 3=already in db
int addnewham(string& calltoadd)
{
string callbuf, namebuf, citybuf;
// for auto-addition, must: have length, be valid, and not already there
if(calltoadd.length() && validcallsign(calltoadd) && !findham(calltoadd))
{
callbuf = calltoadd;
scr << setclr(GREEN) << setxy(1,25) << clreol
<< "Adding " << setclr(WHITE) << callbuf << setclr(GREEN)
<< ", Enter " << setclr(WHITE);
}
else // need user entry of call sign
{
scr << setclr(WHITE) << setxy(1,25) << clreol
<< "CALL SIGN" << setclr(GREEN) << " to add or <CR> to quit: "
<< setclr(WHITE);
if(!cgets(callbuf)) // allow ABORT
return(1);
if(!validcallsign(callbuf)) // invalid
return(2);
if(findham(callbuf)) // already there
return(3);
scr << setxy(1,25) << clreol;
}
scr << "NAME " << setclr(GREEN) << "or <CR> to quit: " << setclr(WHITE);
if(!cgets(namebuf)) // allow ABORT
return(1);
scr << setclr(WHITE) << setxy(1,25) << clreol
<< "CITY " << setclr(GREEN) << "or <CR> for " << setclr(WHITE)
<< cities[cindex]->n << setclr(GREEN) << ": " << setclr(WHITE);
if(!cgets(citybuf))
citybuf = cities[cindex]->n;
HAM* newham = new HAM(callbuf,namebuf,findcity(citybuf)); // got this far,
db.Add(newham); // it's an addition for sure.
do
{
scr << setxy(1,25) << clreol << "Checkin code \"ACLOPTVX \": ";
newham->ck = tolower(getche());
}
while(!strchr("acloptvx ",newham->ck)); // DEFAULT NOT ALLOWED HERE
if(newham->ck != 'v') // IF NOT A VISITOR,
dbchanged = TRUE; // WE'LL HAVE TO UPDATE CALLS.DAT FOR SURE,
if(newham->ck != ' ') // assign PLACE IN THE CHECKIN ORDER
newham->ord = ckinord++;
handletac(newham); // PROCESS TRAFFIC, ANNOUNCEMENT, OR CONTACT
if(extendedlog) // make a log entry of the checkin
if(!strchr("tac",newham->ck)) // t,a,c, already memoed in handletac
{
string memostart = newham->call + " Ck: " + newham->ck;
memo(memostart,TRUE);
}
return(0);
} // addnewham()
//----------------------------------------------------------------------------
// EDIT A DB RECORD
// RETURNS: TRUE IF RECORD WAS FOUND IN DB, FALSE IF NOT
int editrec(string& tofind) // CALL SIGN TO FIND AND EDIT
{
int needsort = FALSE; // true if any sort field is changed
string buf;
HAM* found = findham(tofind); // pointer to node matching call sign (if any)
if(!found) // CAN'T EDIT A RECORD THAT'S NOT THERE
return(FALSE);
scr << setclr(WHITE) << setxy(1,25) << clreol
<< "Correct CALL or <CR> for " << found->call << ": ";
if(cgets(buf) && validcallsign(buf)) // if invalid call, just ignore input
{
needsort = TRUE;
found->call = to_upper(buf);
}
scr << setxy(1,25) << clreol << "Correct NAME, or <CR> for " << found->name << ": ";
if(cgets(buf))
found->name = to_upper(buf); // leaves name all upper case during the net
scr << setxy(1,25) << clreol << "Correct CITY, or <CR> for " << found->city->n << ": ";
if(cgets(buf))
found->city = findcity(buf);
if(needsort) // if sort field was changed, have to re-add entry to re-sort.
{
HAM::sort = HAM::bycall; // specify sort order
db.Detach(found,TShouldDelete::NoDelete); // remove from array
db.Add(found); // and sort it back in.
}
dbchanged = TRUE; // MARK DB AS CHANGED
return(TRUE);
} // editrec()
//----------------------------------------------------------------------------
// display the current help line on line 25
void showhelpline()
{
scr << setclr(GREEN) << setxy(1,25) << clreol << helpline;
}
//----------------------------------------------------------------------------
// DISPLAY ONE DB RECORD IN FORMAT USED ON INPUT SCREENS
void displayrec(HAM* h) // RECORD BEING DISPLAYED
{
int color = RED; // INITial COLOR,
if(h->ck > ' ') color = LIGHTBLUE; // then BUMP UP
if(h->need > ' ') color = MAGENTA; // ON INCREASING
if(strchr("ta",h->ck)) color = YELLOW; // IMPORTANCE LEVELS
scr << setclr(color) << h->ck << h->need << " "
<< setw(6) << setiosflags(ios::left) << h->call << " "
<< setw(9) << setiosflags(ios::left) << h->name;
}
//----------------------------------------------------------------------------
// LIST EVERYONE WHO'S CHECKED IN
// RETURNS: A CHAR (int) THAT CAN BE A VALID MAIN MENU COMMAND
int listall(int s) // SORT ORDER: A=ALPHA(call sign), OTHER=CHECKIN ORDER
{
fi.Flush(TShouldDelete::NoDelete); // start empty
HAM::sort = (tolower(s) == 'a') ? HAM::bycall : HAM::byord; // specify sort order
int count = db.GetItemsInContainer();
for(int i = 0 ; i < count ; i++) // SEARCH DB,
if(db[i]->ck > ' ') // IF A CHECKIN,
fi.Add(db[i]); // ADD IT TO FOUND ARRAY
scr.clrscr();
int traffic = 0; // COUNT OF ANNOUNCEMENTS + TRAFFIC
int checkins = fi.GetItemsInContainer(); // # CHECKINS
for(i = 0 ; i < checkins ; i++) // DISPLAY THE LIST
{
if(i && !(i % 96)) // ALLOW FULL LIST DISPLAY USING MULTIPLE PAGES,
{ // BUT NO PAGING BACK THROUGH LIST
scr << setclr(LIGHTGRAY) << setxy(1,25) << clreol << "More...";
getch();
scr.clrscr();
} // ROWS 1-24 AND COLUMNS 0,20,40,60
scr << setxy(((i % 96) / 24) * 20 + 1,((i % 96) % 24) + 1);
displayrec(fi[i]);
if(strchr("ta",fi[i]->ck))
traffic++;
}
scr << setclr(LIGHTGRAY) << setxy(1,25) << clreol
<< checkins << " Checkins. " << traffic << " Traffic. "
<< setclr(GREEN) << "Enter command, or <CR> for menu: " << setclr(LIGHTGRAY);
int ch = ci(); // value to return
// scr << setxy(1,25) << clreol;
return(ch);
} // listall()
//----------------------------------------------------------------------------
// FIND CALL SIGNS BY STRING match.
// DISPLAYS ALL MATCHES, sorted by call sign, and ALLOWS MULTIPLE CHECKINS.
// FEATURE NOT SHOWN IN HELP LINE: ACCEPTs CONTROL-CODE CURSOR MOVEMENT COMMANDS,
// TAB CHECKS IN, FOR LEFTHANDED OPERATION.
// RETURNS: 'e' IF an immediate return to this function is needed,
// FALSE IF NO matches were found, TRUE if at least 1 match,
// or a user-entered main menu command.
// remember that ^A=1, same as TRUE (though ^A is currently trapped, not returned)
int callrollcall(string& tomatch) // STRING TO MATCH, OR THE LOWERCASE WORD "ALL"
{
fi.Flush(TShouldDelete::NoDelete); // start empty
HAM::sort = HAM::bycall; // specify sort order
BOOL allflag = (tomatch == "all"); // TRUE = SELECT ALL,
// FALSE = must MATCH SEARCH STRING.
int count = db.GetItemsInContainer();
for(int i = 0 ; i < count ; i++)
if(allflag || db[i]->call.contains(tomatch)) // put CALLS matching STRING,
{ // OR ALL, into subset array.
if(filteron && (db[i]->ck > ' ')) // IF FILTER, OMIT THOSE CHECKED IN
continue;
fi.Add(db[i]); // OTHERWISE ADD TO ARRAY
}
int fc = fi.GetItemsInContainer(); // for speed, often-used later
if(!fc) // NO MATCHES FOUND
return(FALSE);
// falls through to here if any matches found.
int refresh; // whether screen needs refreshing
int ch; // USER INPUT
int page = -1; // -1=FLAG: no PAGE SELECTED YET. 96 ENTRIES/page
i = 0; // i is now an index into fi[]. SET TO FIRST RECORD.
string memostart; // string for starting memo with
helpline =
"2468 7/1=t/b Goto Index Schedule Roster Edit Memo New <CR>=quit \"ACLOPTVX 5\"";
while(1) // ALL EXITS ARE IN SWITCH
{
// The switch cases (later) change i, the index into fi[].
if((i / 96) != page)// IF PAGE CHANGE IS NECESSARY for displaying the new fi[i],
{ // DO IT, AND DRAW the whole SCREEN FOR THAT PAGE.
// K COUNTS THE (UP TO) 96 ENTRIES FOR THE PAGE
page = i / 96; // integer math: first page is page 0
scr.clrscr();
showhelpline();
for(int k = page * 96 ; ((k < fc) && (k < (page * 96 + 96))) ; k++)
{ // 4 COLUMNS
scr << setxy(((k % 96) / 24) * 20 + 1,((k % 96) % 24) + 1);
displayrec(fi[k]);
}
} // PUT CURSOR AT CURRENT RECORD
scr << setxy(((i % 96) / 24) * 20 + 1,((i % 96) % 24) + 1);
switch(ch = ci())
{
case(HOME): // TOP OF PAGE ONLY
case(CTLQ):
i = page * 96; // IF WE'RE ON THE PAGE, WE MUST HAVE
break; // GOTTEN THERE LEGALLY ALREADY
case(END): // BOTTOM OF PAGE ONLY
case(CTLZ):
i = min((page * 96 + 95),fc-1); // PAGE END OR LIST END
break;
case(LEFT): // left arrow
case(CTLA):
if(i-24 < 0) break; // BUT NOT BELOW ARRAY BASE
i -= 24; // PAGE CHANGE CAN OCCUR HERE
break;
case(RIGHT): // RIGHT ARROW
case(CTLF):
if(i+24 >= fc) break; // BUT NOT BEYOND ARRAY end
i += 24; // PAGE CHANGE CAN OCCUR HERE
break;
case(UP): // UP ARROW
case(CTLE):
if(--i < 0) i = fc-1; // WRAP AROUND TO BOTTOM OF ARRAY
break; // PAGE CHANGE CAN OCCUR HERE
case(DOWN): // DOWN ARROW
case(CTLX):
if(++i >= fc) i = 0; // WRAP AROUND TO TOP OF ARRAY
break; // PAGE CHANGE CAN OCCUR HERE
case(PGUP): // previous page (or array top)
case(CTLR):
i = max(i-96,0);
break;
case(PGDN): // next page (or array end)
case(CTLC):
i = min(i+96,fc-1);
break;
case(CHOME): // array top
i = 0;
break;
case(CEND): // array end
i = fc-1;
break;
// LEGAL CHARACTERS FOR CHECKING IN
case 'l': // DEFCH CAN BE ONE OF THESE
case 'o':
case 'p':
case 'x':
defch = ch;
case 'a': // DEFCH CANNOT BE ANY OF THESE
case 'c':
case 't':
case 'v':
// IT'S OK TO TEST .CK BECAUSE IT HASN'T BEEN CHANGED TO CH YET
if(fi[i]->ck == ' ') // ONLY IF IT'S A NEW CHECKIN,
fi[i]->ord = ckinord++; // INCREMENT COUNT
case ' ':
fi[i]->ck = ch; // FINALLY, WE CAN CHANGE .CK
case('\t'): // TAB
case(FIVE): // KEYPAD 5 CHECKS IN WITH DEFCH
// MUST TEST BECAUSE IT MIGHT HAVE FALLEN THROUGH FROM ABOVE
if((ch == FIVE) || (ch == 9))
{
fi[i]->ck = defch; // CHECKIN WITH DEFCH
fi[i]->ord = ckinord++; // AND LOG CHECKIN ORDER
}
refresh = handletac(fi[i]); // PROCESS T, A, OR C
// update this record for sure, because even if we return,
// this record is usually left displayed, and should be accurate
scr << setxy(((i % 96) / 24) * 20 + 1,((i % 96) % 24) + 1);
displayrec(fi[i]); // update screen info and color
if(extendedlog) // make a log entry of the checkin
if(!strchr("tac",fi[i]->ck)) // t,a,c, already memoed in handletac
{
memostart = fi[i]->call + " Ck: " + fi[i]->ck;
memo(memostart,TRUE);
}
if(fc == 1) // user SELECTED THE ONLY MATCH
return(TRUE); // so return for another search
if(refresh) // If any other record might be affected,
return('e'); // return and come back to redraw whole page
showhelpline(); // gets here if: fc > 1 AND no other record affected
break;
case 'm': // M = CREATE A MEMO
memo(memostart.remove(0),FALSE);
showhelpline();
break;
case 'e': // edit the selected record
editrec(fi[i]->call); // and return because changed info
return('e'); // makes current screen obsolete.
case('\r'): // RETURN FROM FUNCTION - to main menu
return(TRUE);
case 'h': // reminder, special: toggle <H>ide flag AND come back
default: // MAIN MENU COMMANDS, BUT ALSO MEANS MISTAKES CAUSE EXIT
// fi.Flush(TShouldDelete::NoDelete); // saves memory, but wastes time
return(ch);
} // END SWITCH
} // END WHILE(1)
} // callrollcall()
//----------------------------------------------------------------------------
// screen title for a city display: city name, traffic count, time
void citytitle()
{
char buf[10]; // for time display
scr.clrscr();
scr << setclr(WHITE) << cities[cindex]->n; // CITY NAME HEADING
if(cities[cindex]->tcount) // SHOW TRAFFIC count
scr << setbk(BLUE) << setclr(WHITE) << setxy(35,1)
<< ((int)(cities[cindex]->tcount)) << " TRAFFIC" << setbk(BLACK);
scr << setclr(GREEN) << setxy(70,1) << _strtime(buf); // show current time
}
//----------------------------------------------------------------------------
// DRAW PAGE WITH INFO FOR ONE CITY AND ALLOW CHECKINS FOR THAT CITY
// FEATURES NOT SHOWN IN HELP LINE: ACCEPTs CONTROL-CODE CURSOR MOVEMENT COMMANDS,
// and TAB CHECKS IN, FOR LEFTHANDED OPERATION
// RETURNS: int, WHICH CAN DETERMINE NEXT CITY TO SHOW, IF ANY,
// OR BE A VALID MAIN MENU COMMAND
int cityrollcall()
{
string memostart; // string for starting memo with
// some cases (below) return only to come right back (to redraw screen).
// When that is the case, previousindex ensures that the cursor is repositioned
// on the same record it was on. It is always 0 (so cursor starts at db[0]),
// unless it was set to a different desired value before returning.
static int previousindex = 0;
fi.Flush(TShouldDelete::NoDelete); // start empty
HAM::sort = HAM::bycitycall; // specify sort order
int count = db.GetItemsInContainer();
for(int i = 0 ; i < count ; i++)
if(db[i]->city == cities[cindex]) // put CALLS for this city
{ // into subset array.
if(filteron && (db[i]->ck > ' ')) // IF FILTER, OMIT THOSE CHECKED IN
continue;
fi.Add(db[i]);
}
citytitle(); // page heading (title,traffic,time)
int ch; // USER INPUT
helpline = "NO ENTRIES: 3/9=+/- <CR>=quit Find Index Edit Memo New ==resume";
count = fi.GetItemsInContainer(); // # entries for this city (often-used later)
if(!count) // IF NO entries, CAN ONLY ADD, OR DECIDE HOW TO RETURN
while(1)
{
showhelpline();
switch(ch = ci())
{
case 'm': // M = CREATE A MEMO
memo(memostart.remove(0),FALSE);
break;
case '=': // RESUME WITH RESUMECITY
return('g');// THIS WILL BECOME ALTCH IN MAIN
case 'n': // add new call to this city (or another) & returns here
default: // INCLUDES ALL VALID COMMANDS PROCESSED IN MAIN
return(ch);
}
}
// CITY HAS ENTRIES
helpline = "2468 3+9-7t1b CR=quit Find Index Edit Memo New ==resume \"ACLOPTVX 5\"";
int page = -1; // FLAG THAT A PAGE HASN'T BEEN SELECTED YET. 92 ENTRIES/page
int refresh;
// i is now reused as an index into fi[].
i = min(previousindex,count-1); // not beyond end; editing can shrink city list
previousindex = 0; // it is set to nonzero for "return and come right back"
while(1)
{
if((i / 92) != page) // SEE IF PAGE CHANGE IS NECESSARY
{ // IF SO, CHANGE PAGE AND REDRAW SCREEN
citytitle(); // page heading (title,traffic,time)
showhelpline();
page = i / 92;
for(int k = page * 92 ; ((k < count) && (k < (page * 92 + 92))) ; k++)
{
scr << setxy(((k % 92) / 23) * 20 + 1,((k % 92) % 23) + 2);
displayrec(fi[k]);
}
} // CURSOR TO CURRENT RECORD
scr << setxy(((i % 92) / 23) * 20 + 1,((i % 92) % 23) + 2);
switch(ch = ci())
{
case(HOME): // GO TO TOP OF PAGE ONLY
case(CTLQ):
i = page * 92;
break;
case(END): // GO TO BOTTOM OF PAGE
case(CTLZ):
i = min((page * 92 + 91),count-1);
break;
case(LEFT): // LEFT ARROW
case(CTLA):
if(i-23 < 0) break; // FELL BELOW BASE
i -= 23; // COLUMN ENTRIES 23 RECORDS APART
break; // PAGE CHANGE CAN OCCUR HERE
case(RIGHT): // RIGHT ARROW
case(CTLF):
if(i+23 >= count) break; // BEYOND END OF THIS CITY
i += 23; // PAGE CHANGE CAN OCCUR HERE
break;
case(UP): // UP ARROW
case(CTLE):
if(--i < 0) i = count-1; // WRAP AROUND TO ARRAY BOTTOM
break; // PAGE CHANGE CAN OCCUR HERE
case(DOWN): // DOWN ARROW
case(CTLX):
if(++i >= count) i = 0; // WRAP BACK TO ARRAY TOP
break; // PAGE CHANGE CAN OCCUR HERE
case(CPGUP): // previous page (or array start)
i = max(i-92,0); // these are a little illogical, but 1 city
break; // requiring > 1 page is very rare anyway.
case(CPGDN): // next page (or array end)
i = min(i+92,count-1); // You can also get to next page by
break; // just using arrow keys.
// LEGAL CHECKIN CHARACTERS
case 'l':
case 'o':
case 'p':
case 'x':
defch = ch; // COPY TO DEFAULT
case 'a': // DEFCH CANNOT BE ANY OF THESE
case 'c':
case 't':
case 'v':
if(fi[i]->ck == ' ') // IF A NEW CHECKIN,
fi[i]->ord = ckinord++; // LOG PLACEMENT & INCREMENT
case ' ':
fi[i]->ck = ch;
case('\t'): // TAB
case(FIVE): // KEYPAD 5 CHECKS IN WITH DEFCH
// MUST TEST BECAUSE IT MIGHT BE HERE FROM ABOVE
if((ch == FIVE) || (ch == 9))
{
fi[i]->ck = defch; // LOG IN WITH DEFAULT CHAR
fi[i]->ord = ckinord++; // AND INCREMENT COUNTER
}
refresh = handletac(fi[i]); // PROCESS T, A, OR C
if(extendedlog) // make a log entry of the checkin
if(!strchr("tac",fi[i]->ck))// t,a,c, already memoed in handletac
{
memostart = fi[i]->call + " Ck: " + fi[i]->ck;
memo(memostart,TRUE);
}
if(refresh)
{ // If any other record might be affected,
previousindex = i;// return and come back. Redrawing whole page
return('e'); // updates both this record and contact, if any.
} // otherwise, just update this record, for speed.
scr << setxy(((i % 92) / 23) * 20 + 1,((i % 92) % 23) + 2);
displayrec(fi[i]);
showhelpline();
break;
case '=': // RETURN TO RESUMECITY
case(F2):
case 'g':
return('g'); // THIS WILL BECOME ALTCH IN MAIN
case 'm': // M = CREATE A MEMO
memo(memostart.remove(0),FALSE);
showhelpline();
break;
case 'e': // edit the selected record and return
editrec(fi[i]->call); // because changed info makes the current
previousindex = i; // screen possibly obsolete.
return('e'); // 'e' means come right back.
// reminders that these are reserved: they're tested for upon returning
case(PGUP): // previous city
case(CTLR):
case(PGDN): // next city
case(CTLC):
case(CHOME): // first city
case(CEND): // last city
case 'h': // toggle <h>ide filter and come back here
case 'n': // adds new call to this city (or another) & returns here
default: // INCLUDES ALL VALID COMMANDS PROCESSED IN MAIN
return(ch);
} // END SWITCH
} // END WHILE CHARACTERS ARE BEING ENTERED
} // cityrollcall()
//----------------------------------------------------------------------------
// CITYMENU() LIST OF CITIES TO SELECT FROM
// RETURNS: CHAR(int) -- 'g' IF SHOULD GO TO DOCITY() UPON RETURN,
// OR RETURNS ANY CHAR ENTERED, WHICH CAN BE A MAIN MENU COMMAND
int citymenu()
{
int ch; // USER INPUT
int page = -1; // FLAG: a PAGE HASN'T BEEN SELECTED YET. 96 ENTRIES/page
int i = cindex; // index into cities[], start at CINDEX
int citycount = cities.GetItemsInContainer();
char firstletter; // first letter of currently selected city
helpline = "TAB|5|<CR>|Goto PgUp/Dn=NextLetter OTHER=valid command or quit";
while(1) // ALL EXITS ARE IN SWITCH
{
if((i / 96) != page) // IF PAGE CHANGE IS NECESSARY,
{ // DO IT, AND DRAW entire SCREEN FOR THAT PAGE
// K COUNTS THE (UP TO) 96 ENTRIES FOR THE PAGE
scr.clrscr();
showhelpline();
page = (i / 96);
for(int k = page * 96 ; ((k < citycount) && (k < (page * 96 + 96))) ; k++)
{ // 4 COLUMNS
scr << setxy(((k % 96) / 24) * 20 + 1,((k % 96) % 24) + 1);
scr << setclr(cities[k]->tcount ? YELLOW : RED) << cities[k]->n;
}
} // PUT CURSOR AT CURRENT RECORD
scr << setxy(((i % 96) / 24) * 20 + 1,((i % 96) % 24) + 1);
switch(ch = ci())
{
case(HOME): // TOP OF PAGE ONLY
case(CTLQ):
i = page * 96; // IF WE'RE ON THE PAGE, WE MUST HAVE
break; // GOTTEN THERE LEGALLY ALREADY
case(END): // BOTTOM OF PAGE ONLY
case(CTLZ):
i = min(page * 96 + 95,citycount-1); // PAGE END OR LIST END
break;
case(LEFT): // LEFT ARROW
case(CTLA):
if(i-24 < 0) break; // BUT NOT BELOW ARRAY BASE
i -= 24;
break;
case(RIGHT): // RIGHT ARROW
case(CTLF):
if(i+24 > citycount-1) break; // BUT NOT BEYOND ARRAY TOP
i += 24;
break;
case(UP): // UP ARROW
case(CTLE):
if(--i < 0) i = citycount-1; // WRAP AROUND TO BOTTOM OF ARRAY
break;
case(DOWN): // DOWN ARROW
case(CTLX):
if(++i > citycount-1) i = 0; // WRAP AROUND TO TOP OF ARRAY
break;
case(PGUP): // previous letter in alphabet
case(CTLR):
firstletter = cities[i]->n[0]; // save current 1st letter
while((i > 0) && (cities[i]->n[0] == firstletter))
i--;
break;
case(PGDN): // next letter in alphabet
case(CTLC):
firstletter = cities[i]->n[0]; // save current 1st letter
while((i < citycount-1) && (cities[i]->n[0] == firstletter))
i++;
break;
case '\t': // TAB = SELECT & GO TO THIS CITY
case '\r': // CR = same
case 'g': // Goto
case(FIVE): // KEYPAD 5 = SAME
cindex = i;
return('g');
default:
return(ch); // ALLOW MAIN MENU COMMANDS
} // END SWITCH
} // END WHILE(1)
} // citymenu()
//----------------------------------------------------------------------------
// READ CALL SIGN database from FILE and create the CITIES[] ARRAY
void loaddb()
{
string citybuf, callbuf, namebuf;
ifstream infile("CALLS.DAT");
if(!infile)
aborts("File CALLS.DAT not found.");
while(infile) // LOAD DATA INTO DB
{ // file format: city,call,name\n
getline(infile,citybuf,','); // fields must not contain imbedded commas.
getline(infile,callbuf,',');
getline(infile,namebuf,'\n');
if(infile.fail())
aborts("File CALLS.DAT is corrupt. Repair and rerun.");
infile >> ws;
if(validcallsign(callbuf)) // if valid and not already in db
if(!findham(callbuf))
db.Add(new HAM(callbuf,namebuf,findcity(citybuf)));
}
} // loaddb()
//----------------------------------------------------------------------------
// WRITE CONTENTS OF DATABASE TO CALLS.DAT in city-sort order.
// Since db[] is in callsign order, it goes through the city list,
// then writes out all the call signs for that city.
void writedb()
{
unlink("CALLS.BAK");
rename("CALLS.DAT","CALLS.BAK");
ofstream outfile("CALLS.DAT");
for(int j = 0 ; j < cities.GetItemsInContainer() ; j++) // for each city,
for(int i = 0 ; i < db.GetItemsInContainer() ; i++) // write its calls to disk
if(db[i]->city == cities[j]) // if it matches this city,
if(db[i]->ck != 'v') // (EXCLUDE VISITORS)
outfile << db[i]->city->n << "," << db[i]->call << ","
<< to_upper(db[i]->name) << endl;
if(outfile.fail())
{
cerr << "Possible data loss in CALLS.DAT." << endl;
presskey();
}
} // writedb
//----------------------------------------------------------------------------
// write the checkin log (record of who checked in) to disk file mm-dd-yy.log
// if extendedlog is on, that detailed log is kept in the (.MEM) memo file.
void writelog()
{
int checkins = 0, traffic = 0; // SESSION TOTALS
string buf;
char obuf[80];
ostrstream os(obuf,sizeof(obuf));
today(buf); // GET DOS DATE
buf += ".LOG"; // CREATE .LOG FILE USING TODAY'S DATE
ofstream outfile(buf.c_str());
for(int i = 0 ; i < db.GetItemsInContainer() ; i++)
if(db[i]->ck > ' ')
{
checkins++;
if(strchr("at",db[i]->ck))
traffic++; // traffic count not accurate
outfile << (char)(toupper(db[i]->ck)) << "," << db[i]->call
<< "," << db[i]->name << endl;
}
if(outfile.fail())
{
cerr << "Possible data loss in " << buf << endl;
presskey();
}
// create a memo with session totals
os << "Session totals: Checkins: " << checkins << " Traffic: " << traffic << ends;
buf = os.str();
memo(buf,TRUE);
}
//----------------------------------------------------------------------------
// load agenda text from disk.
void loadagenda()
{
ifstream infile("preamble.txt");
if(!infile)
aborts("File PREAMBLE.TXT not found.");
string buf;
while(getline(infile,buf,'\n'))
agenda.Add(buf);
} // loadagenda
//----------------------------------------------------------------------------
// print formatted roster to disk, for printing to paper.
void printroster()
{
ofstream outfile("CALLS.PRN");
for(int j = 0 ; j < cities.GetItemsInContainer() ; j++) // for each city
{
outfile << "\n--> " << cities[j]->n << "\n\n"; // city heading
for(int i = 0 ; i < db.GetItemsInContainer() ; i++) // write its calls to disk
if(db[i]->city == cities[j]) // if it matches this city,
if(db[i]->ck != 'v') // (EXCLUDE VISITORS)
outfile << " " << setiosflags(ios::left) << setw(6)
<< db[i]->call << " " << to_upper(db[i]->name) << endl;
}
} // printroster()
//----------------------------------------------------------------------------
// DRAWHELP() DISPLAYS HELP SCREEN
void drawhelp()
{
char buf[9]; // HOLDS TIME
int checkins = 0; // SESSION TOTALS
int traffic = 0;
for(int i = 0 ; i < db.GetItemsInContainer() ; i++)
if(db[i]->ck > ' ')
{
checkins++;
if(strchr("at",db[i]->ck))
traffic++;
}
// BJKUYZ // unused command letters
scr.clrscr();
scr << setclr(LIGHTGRAY);
scr << "Call Signs: F <F>ind CALL SIGN(S) by partial string match (F1)" << endl;
scr << " E <E>dit a record" << endl;
scr << " N <N>ew person for database" << endl;
scr << " R <R>oster (show entire call sign list)" << endl;
scr << endl;
scr << "Cities: G <G>oto (F2): " << setclr(GREEN) << cities[cindex]->n
<< setclr(LIGHTGRAY) << endl;
scr << " I <I>ndex of cities (F6)" << endl;
scr << endl;
scr << "Utility: S <S>chedule for the net (agenda) (F4)" << endl;
scr << " L,A <L>ist checkins in order or by <A>lpha" << endl;
scr << endl;
scr << "Memos: <M>emo entry <D>isplay memos <W>rite to disk" << endl;
scr << endl;
scr << "Switches: <H>ide checkins:"
<< setclr(GREEN) << (filteron?"YES":"NO") << setclr(LIGHTGRAY);
scr << " <^V>erify calls:"
<< setclr(GREEN) << (verifycalls?"YES":"NO") << setclr(LIGHTGRAY);
scr << " Extended <^L>og:"
<< setclr(GREEN) << (extendedlog?"YES":"NO") << setclr(LIGHTGRAY) << endl;
scr << endl;
scr << "System: P <P>rint formatted roster to disk" << endl;
scr << " F9 run COMMAND.COM (temporarily exit to DOS)" << endl;
scr << " Q <Q>uit program. " << endl;
scr << endl;
scr << "Checkin Codes: <X> Regular checkin <space> Remove checkin" << endl;
scr << " <A>nnouncement <C>ontact <L>ate <O>ut <P>ortable"
<< endl;
scr << " <T>raffic <V>isitor (won't write to disk) "
<< endl;
scr << " <TAB> or <Keypad5> Check in with current default" << endl;
scr << setclr(LIGHTGRAY);
scr << setxy(40,25) << "Checkins: " << setclr(GREEN) << checkins << setclr(LIGHTGRAY);
scr << " Traffic: " << setclr(GREEN) << traffic;
scr << setxy(70,25) << _strtime(buf) << setclr(LIGHTGRAY)
<< setxy(1,25) << "Command: ";
} // drawhelp()
//----------------------------------------------------------------------------
// MAIN()
int main()
{
int i; // COUNTER
int ch; // USER INPUT
int altch = 0; // RETURNED BY FUNCTION TO INDICATE TO GO DIRECTLY
// TO NEXT FUNCTION. BYPASSES USER INPUT.
int agendastart = 0; // line # with which to start display
char buf2[10]; // for TIME display
string buf; // user input, filenames, etc.
// video display
scr.window(1,1,80,25);
scr.textmode(C80);
scr << setbk(BLACK) << setclr(LIGHTGRAY);
scr.clrscr();
string::set_paranoid_check(1);
loadagenda(); // load net agenda text
loaddb(); // LOAD CALLS.DAT, CREATE CITIES LIST
cindex = 0; // re-point at 1st entry (in case calls.dat isn't in city order)
while(1)
{
scr << setclr(LIGHTGRAY); // prevents cursor in a "leftover" color
if(altch) // IF A FUNCTION SET ALTCH, WE GO DIRECTLY TO THAT CASE,
{ // BYPASSING USER INPUT
ch = altch; // TRANSFER TO CH
altch = 0; // RESET ALTCH
}
else // NEED USER INPUT
{
drawhelp();
ch = ci();
}
switch(ch)
{
//----------------------------------------------------------------------------
// major searches: city, bycall, bycity
case 'i':
case(F6): // SELECT FROM CITY MENU
altch = citymenu(); // MENU COMMANDS ALLOWED
break;
case(F1):
case('f'): // CALL SIGN match
case('r'): // ENTIRE roster
buf.remove(0); // empty buf means we need user input
while(!altch) // EXITS loop if USER ABORTS OR ALTCH GETS SET
{
if(!buf.length()) // if not a repeat search from below
if(ch == 'r') // whole roster REQUIRES NO INPUT
buf = "all";
else
{
scr << setclr(WHITE) << setxy(1,25) << clreol
<< "Find Call or <CR> to quit: ";
if(!cgets(buf)) // USER ABORTED
break;
buf.to_upper();
}
switch(ch = callrollcall(buf))
{
case(FALSE): // returns FALSE if no matches
addnewham(buf); // no match: probably need to add a call,
buf.remove(0); // empty buf signals a new search
break;
case(TRUE): // returns TRUE if at least 1 match
buf.remove(0); // empty buf signals a new search
break;
case 'h': // TOGGLE "hide" FILTER
filteron = !filteron; // and leave buf with contents
break; // to repeat the previous search
case 'e': // a record was changed:
break; // go back to redo same search
// ANYTHING ELSE IS A COMMAND
default: // TO EXIT the while LOOP
altch = ch; // AND GO ELSEWHERE
break;
}
} // END WHILE NOT ALTCH
break;
case 'g': // show page for 1 city
case(F2):
// && EVALS LEFT TO RIGHT. IF ALTCH, cityrollcall() NOT DONE
while(!altch && ((ch = cityrollcall()) != '\r'))
{
switch(ch)
{
// any case that doesn't set cindex or set altch
// will cause a return to the same city just processed
case(PGDN): // KEYPAD 3 = GO TO NEXT CITY
case(CTLC):
cindex++; // TEST FOR OVER-RUN IS BELOW
resumecity = cindex; // LAST CITY REACHED
break;
case(PGUP): // GO TO PREVIOUS CITY
case(CTLR):
cindex = max(cindex-1,0); // BUT NOT BELOW 0
break;
case(CHOME): // go to 1st city
cindex = 0;
break;
case(CEND): // go to last city
cindex = cities.GetItemsInContainer()-1;
break;
case 'h': // change filter
filteron = !filteron; // go back to same city
break;
case 'e': // a record was edited,
break; // go back to display same city.
case 'n': // ADD NEW CALL TO THIS CITY
addnewham(buf.remove(0)); // it will THEN REDRAW THE CITY
break; // AND GO BACK TO IT
case 'g': // char returned here when '=' is chosen
cindex = resumecity; // do cityrollcall(resumecity)
default: // DEFAULT INCLUDES
altch = ch; // MENU COMMANDS RETURNED
break;
} // END SWITCH
if(cindex >= cities.GetItemsInContainer()) // LAST CITY WAS DONE
{ // AND WAS EXITED WITH PGDN
resumecity = cindex = 0; // RESET POINTERS
altch = 'f'; // GO DO (find) LATE/MISSED
}
} // END WHILE CH = DOCITY() AND NOT ALTCH
break;
//----------------------------------------------------------------------------
// agenda display
case(F4): // show current 23 lines of NET AGENDA
case 's': // schedule
scr.clrscr();
scr << setclr(LIGHTGRAY);
for(i = agendastart ; i < (agendastart+23) ; i++)
{
if(i >= agenda.GetItemsInContainer()) // past end of agenda
break;
scr << agenda[i] << endl;
} // display time
scr << setclr(GREEN) << setxy(70,25) << _strtime(buf2) << "\r";
scr << "PgUp/PgDn, Command, or <CR> for menu: ";
altch = ci(); // ALLOW ALL MENU COMMANDS
switch(altch) // change agenda page, loop again to display it
{
case(PGDN): // next 23 lines
agendastart += 23;
if(agendastart >= agenda.GetItemsInContainer())
agendastart = 0;
altch = 's'; // go display them
break;
case(PGUP): // previous 23 lines
agendastart = max(agendastart - 23,0);
altch = 's'; // go display them
break;
}
break;
//----------------------------------------------------------------------------
// memo
case 'm': // M = ENTER A MEMO
memo(buf.remove(0),FALSE);
break;
case('d'): // DISPLAY MEMOS
altch = showmemos(); // ALLOW MENU COMMANDS
break;
//----------------------------------------------------------------------------
// add, edit, summarize
// this is somewhat superfluous, because you must enter entire call.
// It's easier to Find, select, and use 'e'dit
case('e'): // EDIT A RECORD
scr << setclr(GREEN) << setxy(1,25) << clreol
<< "<EDIT>" << setclr(WHITE) << " Find call or <CR> to quit: ";
if(cgets(buf))
editrec(buf);
break;
case 'n': // ADD NEW PERSON TO DATABASE
if(addnewham(buf.remove(0)) == 3) // if already in database,
altch = 'f'; // give opportunity to Find it.
break;
case 'l': // L = LIST CHECKINS IN NUMERICAL ORDER
case 'a': // A = LIST CHECKINS IN ALPHA ORDER
altch = listall(ch); // ALLOW MENU COMMANDS
break;
//----------------------------------------------------------------------------
// utility
case 'h': filteron = !filteron; break; // TOGGLE hide checkins
case(CTLV): verifycalls = !verifycalls; break; // toggle call sign verify
case(CTLL): extendedlog = !extendedlog; break; // toggle extended logging
case 'p': printroster(); break; // print roster to disk
case 'w': writememos(); break; // Could also writedb and writelog
case(F9): // RUN COMMAND.COM
scr.clrscr(); // This works even when in a DOS session
system("command.com"); // under Windows, and is the only way to
scr.clrscr(); // exit to DOS in that situation, (because
break; // DOS is already occupied running Net)
case 'q': // Q QUIT PROGRAM
scr << setclr(WHITE) << setxy(1,25) << clreol
<< "QUIT PROGRAM. Press ESC to quit: ";
if(getche() != 27)
{
scr << setxy(1,25) << clreol;
break;
}
scr.clrscr();
writelog(); // write checkin log to disk
if(dbchanged) // write calls.dat to disk
writedb();
writememos(); // write memos to disk
return(0);
default:
break;
} // END SWITCH
} // END WHILE CHARS ARE BEING ENTERED
} // END MAIN
/* LOGTALLY.CPP 2-23-96
This file is part of the NET project, but is an optional standalone program.
Copyright (C)1994-1996 Steven Whitney.
Published under GNU GPL (General Public License) Version 2, with ABSOLUTELY NO WARRANTY.
Initially published by http://25yearsofprogramming.com.
POSTS THE DATA FROM A DAILY .LOG FILE (generated by net.cpp) TO A YEARLY MASTER CHECKIN FILE
(that tabulates the year) in the format:
CALLSIGN,NAME,"(366 CHARACTERS, CORRESPONDING TO DAYS IN THE YEAR)"\n
DAILY .LOG FILE NAME MUST HAVE FORMAT MM-DD-YY.LOG.
OUTPUT FILE MUST HAVE FORMAT YYYY.LOG WHERE YYYY IS 4-DIGIT YEAR.
FILES DO NOT HAVE TO BE SORTED, AND ARE NOT SORTED BY PROGRAM.
All files must be in default directory, with this program.
FOR BATCH PROCESSING (to process all daily files at once): FOR %F IN (*.LOG) DO LOGTALLY %F
The YYYY.LOG file won't be processed, because its name is the wrong length.
------
TO DO:
I haven't checked to make sure that the master file, after posting, contains
the same info that was in the daily logs!: call sign, checkin char
Assumes years are 19xx. Needs Y2K fix.
*/
#include <classlib\time.h>
#include <classlib\arrays.h>
#pragma hdrstop
#include "c:\bcs\my.h"
#include "c:\bcs\mylib.cpp"
//////////////////////////////////////////////////////////////////////////////
// global variables
struct HAM
{
HAM() {}
HAM(string& c, string& n, char cki) : call(c), name(n), ck(cki) {}
BOOL operator == (const HAM& other) const { return(call == other.call); }
BOOL operator < (const HAM& other) const { return(call < other.call); }
string call; // was char[7]
string name; // was char[11]
char ck; // CHECKIN CHARACTER FOR THAT DAY
};
TISArrayAsVector<HAM> s(50,0,50);
//////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------
// load array s (data to be posted) from daily log file
void loaddaylog(const char* filename)
{
cout << "\nReading daily log...";
ifstream infile(filename);
if(!infile)
aborts("Input file not found.");
string inck, incall, inname;
while(getline(infile,inck,','))
{
getline(infile,incall,',');
getline(infile,inname,'\n');
if(infile.fail())
aborts("Log file corrupt. Repair and rerun.");
infile >> ws;
s.Add(new HAM(incall,inname,inck[0]));
}
}
//////////////////////////////////////////////////////////////////////////////
// MAIN()
int main(int argc, char **argv)
{
int i;
cout << "Usage: LOGTALLY MM-DD-YY.LOG (include .LOG extension)";
if(argc != 2)
exit(1);
string::set_case_sensitive(0);
string::set_paranoid_check(1);
string day = argv[1]; // mm-dd-yy
if((day.length() != 12) || (!day.contains(".log"))) // also screens out YYYY.LOG
exit(1);
day.remove(8); // strip extension
string year = "19"; // 19XX
year += (argv[1]+6); // add YY from mm-dd-yy
year.remove(4); // strip extension
TDate thisdate(atoi(argv[1]+3),atoi(argv[1]),atoi(year.c_str())+atoi(argv[1]+6));
int thisdatesindex = thisdate.Day()-1; // location within yrdata of char for this date
loaddaylog(argv[1]); // READ DATA FROM DAILY .LOG FILE INTO ARRAY S
//----------------------------------------------------------------------------
// post the one-day information from array s to the master file
cout << "\nPosting to master file...";
ofstream outfile;
ifstream infile((year + ".log").c_str());
if(!infile) // if master file doesn't exist,
{ // create it
outfile.open((year + ".log").c_str());
outfile.close();
infile.open((year + ".log").c_str());
}
outfile.open((year + ".new").c_str());
string incall, inname, yrdata; // data read from file
// an empty tally array for calls that are new this session.
// can reuse, changing only the char for this log file's day.
string newyrdata(' ',366); // 366 spaces
while(getline(infile,incall,',')) // GET RECORD FROM MASTER file
{
getline(infile,inname,',');
infile.ignore(1); // opening quote
getline(infile,yrdata,'\"'); // ending quote
if(infile.fail())
aborts("Master (year) file corrupt. Repair and rerun.");
infile >> ws;
// See if any records in s should be written before this one, to keep sorted.
// There's no i++ in loop: records can be deleted, compacting s.
// It's always s[0] that gets written!
for(i = 0 ; i < s.GetItemsInContainer() ; )
if(s[i]->call < incall) // precedes call we're looking for,
{ // so insert before it...
newyrdata[thisdatesindex] = s[i]->ck;
outfile << s[i]->call << "," << s[i]->name << "," << "\""
<< newyrdata << "\"" << endl;
s.Destroy(i); // i remains the same
}
else
if(s[i]->call == incall) // matches, UPDATE MASTER RECORD
{
yrdata[thisdatesindex] = s[i]->ck; // SET ck for this DATE
s.Destroy(i); // POSTED, no longer needed
break; // i remains the same
}
else // s[i]->call > incall, so quit looking
break;
// WRITE (POSSIBLY UPDATED) RECORD TO NEW MASTER
outfile << incall << "," << inname << "," << "\"" << yrdata << "\"" << endl;
}
// Write any remaining new records after last master file record in alphabet.
for(i = 0 ; i < s.GetItemsInContainer() ; i++)
{
newyrdata[thisdatesindex] = s[i]->ck;
outfile << s[i]->call << "," << s[i]->name << "," << "\""
<< newyrdata << "\"" << endl;
}
if(!outfile)
aborts("File write error. Possible data loss.");
infile.close();
outfile.close();
//----------------------------------------------------------------------------
unlink((year + ".bak").c_str()); // DELETE .BAK IF PRESENT
rename((year + ".log").c_str(),(year + ".bak").c_str()); // .LOG TO .BAK
rename((year + ".new").c_str(),(year + ".log").c_str()); // .NEW TO .LOG
cout << "\nDone.\n";
return(0);
}
/* LOGSPRED.CPP 2-23-96
This file is part of the NET project, but is an optional standalone program.
Copyright (C)1994-1996 Steven Whitney.
Published under GNU GPL (General Public License) Version 2, with ABSOLUTELY NO WARRANTY.
Initially published by http://25yearsofprogramming.com.
FORMATS THE DATA FROM A YEARLY SUMMARY .LOG FILE (YYYY.LOG, generated by logtally.cpp)
INTO A SPREADSHEET-TYPE FILE SUITABLE FOR PRINTING (YYYY.PRN). ONLY DATES FOR WHICH THERE ARE
ANY CHECKINS AT ALL WILL BE TABULATED SO THAT ENTIRE BLOCKS OF BLANKS WON'T BE OUTPUT.
PRINTING IT WILL REQUIRE YET ANOTHER PROGRAM TO PRODUCE MULTIPLE 80-COLUMN PAGES THAT CAN BE
TAPED TOGETHER.
SAMPLE OUTPUT:
Summary checkin log for 1993:
Day of Week: SMTWtFs (THURSDAY AND SATURDAY TO BE LOWER CASE)
MONTH: 0000000 READING DOWN: 01=JANUARY
1111111
DAY: 0000000 READING DOWN: JANUARY 01,02,03,ETC.
1234567
CALL NAME XLAOTCX Letters encode the method that person used to check in each day.
CALL NAME LLXXOXP Ex.: Late, Late, Normal, Normal, In/Out, Normal, Portable...
------
TO DO:
I haven't checked to make sure that the PRN file contains
the same info that was in the YYYY.LOG file: call signs, checkin char
The first conversion (from the original C version), for unknown reasons, wrote day of week
headings for the entire year, and also wrote varying-length (and wrong) lines to the
file for the (call sign/checkin info) line. The methods have been completely
changed, but there remains the possibility (or does there?) that
the file-write routines can be confused by very long output lines.
On the other hand, the master file ALWAYS contains entire-year info for
each call sign, and it works ok so far.
Delete this note after it hasn't happened for a while.
Check for any Y2K problems.
*/
#include <classlib\time.h>
#pragma hdrstop
#include "c:\bcs\my.h"
#include "c:\bcs\mylib.cpp"
//////////////////////////////////////////////////////////////////////////////
int main(int argc, char **argv)
{
int i, j;
string infilename;
cout << "Use: LOGSPRED YYYY.LOG where yyyy.log is a yearly log file";
if(argc < 2)
infilename = "1996.log"; // for testing
else
infilename = argv[1];
string daylets("MTWtFsS"); // first letters of day names
int daycount[366]; // daily checkin totals
for(i = 0 ; i < 366 ; i++)
daycount[i] = 0; // and initialize all counts to 0
//----------------------------------------------------------------------------
// read file once through to LEARN WHICH DAYS HAVE CHECKINS and get day totals
ifstream infile(infilename.c_str());
if(!infile)
aborts("Input file not found.");
string call, name, yrdata; // DATA FROM 19XX.LOG FILE
while(getline(infile,call,','))
{
getline(infile,name,',');
infile.ignore(1);
getline(infile,yrdata,'\"');
if(infile.fail())
aborts("Input file corrupt. Repair and rerun.");
infile >> ws;
for(i = 0 ; i < 366 ; i++) // IF THERE WAS A CHECKIN THIS DAY,
if(yrdata[i] > ' ') // MARK IT AS A DAY TO REPORT ON
daycount[i]++; // and increment the checkin count this day
}
infile.close();
// set up OUTPUT FILE
string year(infilename.c_str()); // 19XX.LOG
year.remove(4); // remove extension
ofstream outfile((year + ".prn").c_str()); // output is 19XX.PRN
outfile << "Summary checkin log for " << year << endl;
//----------------------------------------------------------------------------
// day headings: SMTWtFs
outfile << "Day of Week: "; // 20 SPACES ALLOWs FOR CALL + NAME
for(i = 0 ; i < 366 ; i++)
if(daycount[i])
{
TDate thisdate(i+1,atoi(year.c_str()));
outfile << daylets[thisdate.WeekDay()-1];
}
outfile << endl;
//----------------------------------------------------------------------------
// create MMDDYY DATES, read vertically. That is, after the 4 lines
string dd[4]; // are printed, you can read the dates vertically down.
for(i = 0 ; i < 366 ; i++) // julian dates
if(daycount[i]) // IF THE DAY HAS ENTRIES
{
char buf[10];
TDate thisdate(i+1,atoi(year.c_str())); // create a date for it
ostrstream os(buf,sizeof(buf)); // format it as MMDD
os << setw(2) << setiosflags(ios::right) << setfill('0') << thisdate.Month()
<< setw(2) << setiosflags(ios::right) << setfill('0') << thisdate.DayOfMonth()
<< ends;
for(j = 0 ; j < 4 ; j++) // 1st char goes to dd[0],
dd[j] += buf[j]; // 2nd char goes to dd[1], etc.
}
// output the vertical date headings
for(i = 0 ; i < 4 ; i++) // headings require 4 lines
{
switch(i)
{
// skip over space reserved for call+name, also inserting descriptive text
case 0: outfile << "Month: M "; break;
case 1: outfile << " M "; break;
case 2: outfile << "Day: D "; break;
case 3: outfile << " D "; break;
}
outfile << dd[i] << endl;
}
outfile << endl;
//----------------------------------------------------------------------------
// PRINT DATA FROM THE .LOG FILE TO LINES OF TEXT
infile.open(infilename.c_str()); // NO TEST: ALREADY FOUND IT ONCE
while(getline(infile,call,',')) // ALREADY READ IT, TOO
{
getline(infile,name,',');
infile.ignore(1); // opening quote
getline(infile,yrdata,'\"');
infile >> ws;
// substr() below limits name to field width. setw() sets minimum width only.
outfile << setw(6) << setiosflags(ios::left) << call << " "
<< setw(12) << setiosflags(ios::left) << name.substr(0,12) << " ";
for(i = 0 ; i < 366 ; i++) // IF THIS IS A DAY WE'RE REPORTING ON,
if(daycount[i])
outfile << yrdata[i]; // OUTPUT THE CHECKIN CHAR
outfile << endl;
}
outfile << endl; // spacer line
//----------------------------------------------------------------------------
// create day total lines, for printing vertically
for(i = 0 ; i < 4 ; i++) // blank vertical date headings for reuse
dd[i].remove(0);
for(i = 0 ; i < 366 ; i++)
if(daycount[i]) // IF THE DAY HAS ENTRIES
{
char buf[10];
ostrstream os(buf,sizeof(buf)); // format it as MMDD
os << setw(4) << setiosflags(ios::right) << setfill(' ') << daycount[i] << ends;
for(j = 0 ; j < 4 ; j++) // 1st digit goes to dd[0],
dd[j] += buf[j]; // 2nd digit goes to dd[1], etc.
}
// vertical print the totals
for(i = 0 ; i < 4 ; i++)
{
switch(i)
{
// skip over space reserved for call+name, also inserting descriptive text
case 0: outfile << "Counts: #1 "; break;
case 1: outfile << "(Read Down) #2 "; break;
case 2: outfile << " #3 "; break;
case 3: outfile << " #4 "; break;
}
outfile << dd[i] << endl;
}
//----------------------------------------------------------------------------
if(outfile.fail())
cout << "File write error. Possible data loss.";
return(0);
} // END MAIN
|
|
|
|
|
|