/* * Program: Fractal Economy Program [fep.c] * Programmer: Robert John Morton UK-YE572246C * Date: Mon 30 May 2022 - Belo Horizonte-MG This source code is exclusively the intellectual property of the programmer. The listing is copiously commented in order to serve as a self-teaching aid for programmers new to the 'C' programming language. Different techniques of achieving the same functionality are included deliberately for illustration. The code has not been tightened too much in order to leave the program easily adaptable to facilitate future modifications and additions. Project: Phase 1: Tab 0: PEOPLE: 30 May 2022 to 09 Jul 2022 [Tue 21 Jun 2022 changed program name from gem.c to fep.c] Phase 2: LAND & ENERGY: 11 Jul to 15 Aug 2022 Phase 3: PROJ & TRADE: 14 Dec 2022 to 06 May 2023 [not full time] Phase 4: MAINT & EQUPT: 07 to 13 May 2023 Phase 5: Full functionality check + listing clean-up 15 to 31 May 2023 From the directory in which 'fep.c' resides: location of the X-windows X11 graphics library | name of the executable output file | | to compile: gcc fep.c -L/usr/X11R6/lib -o fep -lX11 -lm | | link to X-windows X11 graphics objects library | link to mathematics objects library for sin, cos etc. to run: ./fep [runs program as a self-contained unit] The following data files must be present in the 'data' directory [at the same level as 'fep' executable]: batt.dat, cons.dat, crop.dat, diary.dat, fin.dat, land.dat, nad.dat Discourse on this program at: https://robmorton.website/software/fep.html */ #include <X11/Xlib.h> // contains the X11 graphical drawing functions #include <stdio.h> // input output to/from the console and keyboard #include <stdlib.h> // standard functions for the 'C' language #include <string.h> // for initialising strings as character arrays #include <unistd.h> // contains the function usleep() #include <math.h> // sin() cos() asin() atan() atan2() etc. #include <time.h> #include <X11/XKBlib.h> // for Xkb [keyboard input] #define WHITE 0xFFFFFF // colour white for 'selected item' lettering #define GREY 0x999999 // colour grey for 'unselected item' lettering #define BLEY 0xBBCCDD // colour blue-grey for 'instructions' lettering #define DARK 0x333333 // colour for menu item background #define BROWN 0x889966 // colour for menu item background #define BLACK 0x000000 // colour black for clearing graph areas #define YELLOW 0xFFFF00 // colour yellow for menu titles #define YELL 0xBBBB00 // duller yellow for scroll bars #define GREEN 0x00FF00 // colour green for graphical traces and messages #define GRN 0x00AA00 // dark green for scope trace #define BLEEN 0xAADDBB // greeny grey #define GLEEN 0x88EE99 // greeny grey #define PREEN 0x449955 // greeny grey #define RED 0xFF0000 // colour red for graphical traces and messages #define GRED 0xFF6666 // colour reddish for graphical traces and messages #define BLU 0xAA0000 // colour bluish for graphical traces and messages #define AMBER 0xFFA500 // colour for graphical traces and messages #define pi 3.14159265358979323846264338327950288 // circular constant π #define RPD 0.01745329252 // number of radians per degree #define SPR 206264.806247 // seconds of arc per radian 648000/π Display *XD; // pointer to an X11 display device Window XW; // reference for the window created for this program GC XG; // X11 graphics context within which to write and draw FILE // FILE HANDLES *FA, // for 'nad.dat' 1024-byte records 262144 [0x40000] bytes *FB, // for 'diary.dat' 16-byte records + 16-byte index records *FC, // for 'crop.dat' 64-byte records 65536 [0x10000] bytes *FD, // for 'fin.dat' 32-byte records + 16-byte index records *FS; // general file handle used in index sorts and searches int // VARIABLES PERTAINING TO Tab 7: THE MAINTENANCE TAB a7 = 1, // Tab 7 MAINT button states b7 = 0, c7 = 0, d7 = 0, e7 = 0, f7 = 0, g7 = 0, h7 = 0, // VARIABLES PERTAINING TO TAB 6: THE TRADE TAB a6 = 1, /* the current states of Tab 6's horizontal buttons 0x100 256 DOC HELP button 128 0x80 0x200 512 DATE| |STATUS button 64 0x40 0x400 1024 CLIENT|| ||LIST button 32 0x20 0x800 2048 CROP||| |||NEW button 16 0x10 0x1000 4096 PRICE|||| ||||EDIT button 8 0x2000 8192 QUANTITY||||| |||||PREV button 4 0x4000 16384 VALUE|||||| ||||||NEXT button 2 0x8000 32768 PAID?||||||| |||||||FIND button 1 |||||||| |||||||| b6 00000000 00000000 00000000 00000000 | */ b6 = 256, // vertical button state in TRANS sub-tab g6 = 0, // vertical bias for lines display on SALES ledger r6 = 1, // current transaction record number f6 = 0, // field number of the invoice in TRANS sub-tab C6 = 0, // colour for payment deadline in TRADE->TRANS->LIST sub-sub-tab H6 = 0, // dummy: highest occupied transaction record [not used] l6 = 0, // index record number of currently highlighted list item N6 = 0, // current total number of transactions on-file o6 = 0, // listing order for invoices [default = DOCument number order] S6 = 0, // start byte of current index within the invoice index file V6 = 0, // invoice overdue alert flag W6 = 0, // Byte 3: transaction 'index re-sort needed' flag bits X6 = 200, // left start coord of invoice data fields Y6 = 99, // base line of first invoice data field // VARIABLES PERTAINING TO TAB 5: THE PROJECTS TAB a5 = 1, /* show CROPS sub-tab by default 0x100 256 trawl HELP button 128 0x80 0x200 512 amber| |LIST button 64 0x40 0x400 1024 PLANTED|| ||KILL button 32 0x20 0x800 2048 HARVEST||| |||NEW button 16 0x10 0x1000 4096 DAYS|||| ||||PREV button 8 0x2000 8192 LOC||||| |||||NEXT button 4 0x4000 16384 REF|||||| ||||||TRAWL button 2 0x8000 32768 CROP||||||| |||||||FIND button 1 |||||||| |||||||| b5 00000000 00000000 00000000 00000000 | */ b5 = 0, // vertical button states in CROPS sub-tab N5 = 0, // get number of occupied crop records H5 = 0, // get highest occupied record r5 = 1, // crop number [number of current crop record] l5 = 0, // index record number of currently highlighted list item L5[] = {31,8,8,8,8,8,8,8,8,8}, // field lengths for crops data f5 = 0, // current field number within crop data display p5 = 0, // start byte of CROP field currently being edited q5 = 0, // start byte of current crop record g5 = 0, // initial scroll position on Crops Sub-tab C5 = 0, // colour for harvesting date in the CROPS sub-tab c5 = 0, // planting date d5 = 0, // growing time e5 = 0, // harvest date j5 = 0, // elapsed time B5 = 0, // full content of the above byte R5 = 0, // number of a 'prospectively' new crops record I5 = 0, // byte num of Record 0 of crop.dat containing new record's bit X5 = 256, // x-coordinate of start of after-notes o5 = 4, // listing order for crops [default = REFerence number order] S5 = 0, // start byte of current index within the crop index file V5 = 0, // crop harvesting alert flag W5 = 0, // crop list 're-sort required' flags [not used at the moment] // VARIABLES PERTAINING TO TAB 4: THE EQUIPMENT TAB a4 = 1, // Tab 4 control button states [bottom row of buttons] b4 = 0, // states of the vertical buttons of the UDT tab c4 = 0, // states of the vertical buttons of the FARM tab d4 = 0, // states of the vertical buttons of the NAV tab e4 = 0, // states of the vertical buttons of the SPARE tab f4 = 0, // states of the vertical buttons of the SPARE tab g4 = 0, // states of the vertical buttons of the SPARE tab h4 = 0, // states of the vertical buttons of the SPARE tab i4 = 0, // Tab 4 horizontal button number // VARIABLES PERTAINING TO TAB 3: THE POWER TAB a3 = 1, // Tab 3 control button states H3 = 194, // horizontal start position of graph area V3 = 82, // vertical start position of graph area b3[8][4]; // average power values for different sources and sinks double c3 = 0, // solar declination [in radians] l3 = 0; // latitude of landshare [in radians] int d3 = 0, // solar azimuth [bearing] e3 = 0, // day number of today within current year [01Jan is Day 0] h3 = 0, // current hour of the day [sun time] k3 = 0, // 2 * pi / 365 or 2 * pi / 365 [in radians] p3 = 0, // number of the current 5-minute period of the day q3 = 0, // seconds since midnight [local time] s3 = 0, // sun time [time of day according to the position of the sun] t3 = 0, // Equation of Time offset [seconds] u3 = 0, // landshare's longitudinal time off-set from GMT [seconds] w3 = 0, // current power [in watts] being produced by the PV array x3 = 0, // solar angle of elevation [in degrees] lq = 0, // charge percentage of the battery for the Left-Hand Bus rq = 0, // charge percentage of the battery for the Right-Hand Bus y3 = 30, // average electricity consumption of the landshare in MJ/day z3 = 35, // average green generating capacity per side [right/left] // VARIABLES PERTAINING TO TABS 2: THE WATER TAB a2 = 1, // Tab 2 control button states: 1,2,4,8,16,32,64,128 v2 = 1, // state of rain graph's vertical buttons H2 = 132, // horizontal start point of monthly graphs V2 = 82, // vertical start position of graph area // VARIABLES PERTAINING TO TAB 1: THE LAND TAB a1 = 0, // Tab 1 control button states b1 = 0, // latitude of landshare's central reference point [seconds of arc] c1 = 0, // longitude of landshare's central reference point d1 = 0, // height of landshare's central reference point [metres] r1[360], // radius [in centimetres] to periphery for each bearing degree h1[360], // height [in centimetres] of boundary for each bearing degree /* VARIABLES PERTAINING TO TAB 0: THE PEOPLE TAB 0x100 256 trawl HELP button 128 0x80 0x200 512 search mode| |NOTES button 64 0x40 0x400 1024 Ref entry mode|| ||DIARY button 32 0x20 0x800 2048 selector mode||| |||LIST button 16 0x10 0x1000 4096 INTEG|||| ||||PREV button 8 0x2000 8192 RESET||||| |||||NEXT button 4 0x4000 16384 REGEN|||||| ||||||KILL button 2 ||||||| |||||||NEW button 1 ||||||| |||||||| a0 00000000 00000000 00000000 00000000 | */ a0 = 0, // Tab 0 control button and switch states D0 = 0, // scrolling dispatcher switch 0=NAD, 1=LIST, 2=DIARY, 3=SUPP // NAD variables b0 = 0, // colour states of indexed field names f0 = 0, // current name & address field number [0 to 12] g0 = 0, // field number of search term found by trawler h0 = 0, // index of start of search term found by trawler i0 = 0, // number of the latest found index record I0 = 0, // number of the 1st index record of search range p0 = 0, // start byte of current name & address field q0 = 0, // start byte of current name & address record r0 = 1, // record number of currently displayed name & address N0 = 0, // total number of occupied PEOPLE records H0 = 0, // highest occupied PEOPLE record S0 = 0, // start byte of current index within 'nad.idx' V0 = 0, // dummy: 'people alert' flag [not used at the moment] // LIST variables W0 = 0, // LIST 'index re-sort required' flag bits [not used at the moment] c0 = 0, // LIST column heading button states: 1,2,4,8,16 d0 = 3, // column heading number 0 to 3 [default is 3: REF] k0 = 0, // mouse bias [record number of nad.idx at first line of display l0 = 0, // selected line in whole index for the PEOPLE->LIST sub-tab // DIARY variables [regarded as a virtual Tab 8] e0 = 0, // mouse scroll bias for DIARY x0 = 0, // column heading number 0 to 3 [default is 0: REF] m0 = 1, // DIARY heading button states 1,2,4,8,16,32 [default DAYS] v0 = 0, // states of vertical buttons in DIARY sub-tab 1,2,4,8,16,32,64,128 N8 = 0, // current number of items in the DIARY file H8 = 0, // dummy: highest occupied DIARY record [not used] u0 = 0, // selected line in whole diary for the PEOPLE->DIARY sub-tab V8 = 0, // diary alert flag W8 = 0; // DIARY 'index re-sort required' bits /* Record size 1024 bytes: 561 occupied, 463 for future use. Search indexes are built for fields 08,09,10,11 only. For each name & address field: the 1st character is the number of bytes of content, followed by up to 47, 24 or 37 characters of the content itself. field content ALPHANUMERIC field offset length CONTENT number | | | | FULL NAME 00 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00 HOUSE/BUILDING 48 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 01 STREET/ROAD 96 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 02 DISTRICT/ZONE 144 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 03 CITY/TOWN 192 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 04 COUNTY/MUNICIPALITY 240 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 05 STATE/ETHNIC COUNTRY 288 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 06 SOVEREIGN COUNTRY 336 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 07 POSTAL CODE 384 NAAAAAAAAAAAAAAAAAAAAAAAA 08 [24] FAST SEARCH NAME 409 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 09 [37] PERSONAL IDENTIFIER 447 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 10 COUNTRY CODE [CC] 485 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 11 SELECTOR KEY 523 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 12 L0[] = field lengths of the fields in a name & address [nad] record O0[] = field offsets of the NAD fields from the beginning of the a record P0[] = outer box-widths for the yellow or green boxes that mark a NAD field FIELD NUMBER 00 01 02 03 04 05 06 07 08 09 10 11 12 */ char L0[13] = { 47, 47, 47, 47, 47, 47, 47, 47, 24, 37, 37, 37, 37}; short O0[13] = { 00, 48, 96,144,192,240,288,336,384,409,447,485,523}, P0[13] = {288,288,288,288,288,288,288,288,150,228,228,228,228}; /*record number of 'nad.dat' to which this index entry pertains 'r0' |length of the currently valid content of this field ||field content [up to 14 bytes] This is the field that is sorted. ||-------------- 0000000000000000 16-bit index record [same for all indexes]*/ int tb = 0, // number of the currently displayed Tab [0 to 7] // VARIABLES PERTAINING TO THE KEYBOARD ks = 0, // keyboard's key scan code ka = 0, // ASCII value of current keypress kr = 0, // carriage-return [enter] // VARIABLES PERTAINING TO THE MOUSE mx = 0, // x-coord of mouse click my = 0, // y-coord of mouse click mw = 0, // mouse wheel variable: 1:scroll up, 2:scroll down // VARIABLES PERTAINING TO THE LINE EDITOR ea = 0, // 1: editor active; 0: editor inactive ey = 0, // line editor's current vertical lettering base coordinate ex = 0, // line editor's current horizontal lettering coordinate en = 0, // current length of the line editor's field content ec = 0; // current cursor position [line editor] char le[48], // line editor array st[32], // search term array for T0findNAD(). Saves much stack pushing. // VARIABLES PERTAINING TO TIME AND DATE dm[] = {31,28,31,30,31,30,31,31,30,31,30,31}; // days in each month time_t et; // elapsed time [in seconds since 00:00:00 UDT 01 Jan 1970] struct tm tm; // structure for local time parameters // ANCILLARY FUNCTIONS YE8BB56C ---------------------------------------------- void helpPage(char *S) { // DISPLAY THE HELP PAGE IN THE DEFAULT WEB BROWSER int i, s = strlen(S); /* length of 'fep.html' file path Opens a file or a URL in the user's preferred application: | in this case, in the default web browser. | | base URL of my web site | | */ char C[64] = "xdg-open https://robmorton.website/"; // 35 characters for(i = 0; i < s; i++) // add the file path for 'fep.html' C[i+35] = S[i]; // onto the end of the base URL C[i+35] = ' '; // space after the end of the command C[i+36] = '&'; // place command in 'background' mode C[i+37] = 0; // string-terminating NULL character printf("%s\n",C); // verification print in terminal window system(C); // issue the command to the system } /* GET AND VALIDATE THE RECORD NUMBER, THE SEARCH TERM LENGTH AND THE SEARCH TERM TEXT FROM AN INDEX RECORD. It is assumed that the file pointer of file 'F' has been pre-set to the start byte of the required index record. This function stores the data record number and index content length in the first two bytes of the returned string. */ char *getIdxRec(FILE *F) { static char S[17]; // to hold the search term text int k; for(k = 0; k < 17; k++) // clear the output array S[k] = 0; /* Please note well that standard function fgetc() returns a SIGNED integer formed by treating the lower 8 bits it gets from the char on disk as an UNSIGNED [positive only] quantity in the range 0 to 255. */ int a = fgetc(F); // get data record number from disk [unsigned char] if(a < 1) // if for some reason the record number is < 1 a = 1; // default it to 1: record 1 is the first record if(a > 255) // if the record number is beyond permitted maximum a = 255; // default it to maximum: only 255 records maximum S[0] = a; // set the record number of the main data record int K = fgetc(F); // get length of field content [an unsigned char] if(K > 14) K = 14; // error content length beyond end of field S[1] = K; // length of search term K += 2; // text is stored starting at S[2] for(k = 2; k < K; k++) // load index search string into S[] S[k] = fgetc(F); return S; // return the pointer to search term text } int ratSysDate() { // RETURNS THE CURRENT SYSTEM DATE YYYYMMDD et = time(NULL); // time in seconds since 00:00:00 UDT 01 Jan 1970 tm = *localtime(&et); /* get the local time parameters number of years, counted from the year 1900 | add 1900 to get the complete year number | | shift the year to the left 4 places | | | month number counting from zero | | | | */ return (tm.tm_year + 1900) * 10000 + (tm.tm_mon + 1) * 100 + tm.tm_mday; /* | | | add 1 to get the true month number [so January = 01] | | shift the month number left two places | add in the day of the month [which starts at day 1] */ } void clearTab() { // CLEAR THE TAB AREA XSetForeground(XD,XG,BLACK); XFillRectangle(XD,XW,XG,17,38,484,383); } /* CONVERT A STRING 'S' OF LENGTH 'I' TO UPPER CASE ONLY. Called from 2 places in T0trawl() and 1 place each in T0search() and T0edField(). */ void SUC(int I, char *S) { int c,i; for(i = 0; i < I; i++) // for each character within the string if((c = S[i]) > 96 && c < 123) // if it is a lower case letter S[i] -= 32; // convert it to its upper case equivalent } /* [RE]DISPLAY LINE EDITOR'S CURSOR: called from 1 place each in showLE() and lfrt() and from 2 places each in bksp() and hEnd(). */ void doCursor(int sw) { /* line editor's current horizontal letter coordinate [start of field] | less 1 because ??? | | cursor's current character position in line editor array | | | six horizontal pixels per character | | | | */ int x = ex - 1 + ec * 6, // compute horizontal position of cursor c = GREEN; // show the cursor in bright green if(sw == 0) c = BLACK; // use background colour to erase the cursor /* cursor position within line editor array | current length of content in line editor array | | */ else if(ec > en) ec = en; // ensure cursor is not beyond content length XSetForeground(XD,XG,c); // set colour for the cursor /* line editor's current vertical lettering base | top [y-coord] of cursor line | | bottom [y-coord] of cursor line | | | */ XDrawLine(XD,XW,XG,x,ey-11,x,ey+1); // display the cursor } void showButs() { // SHOW THE 8 BLANK BUTTONS ACROSS THE BOTTOM OF THE TAB int h = 17; // set 'h' to left side of first button XSetForeground(XD,XG,DARK); // shade for buttons' background for(int i = 0; i < 8; i++) { // shade the background area for each button XFillRectangle(XD,XW,XG,h,362,53,21); h += 59; // move across to the next button } } /* GET AN INT FROM A 4-BYTE TRAIN IN A FILE. Called from a large number of places throughout the program */ int getInt(FILE *F) { unsigned int x // need to shift the sign bit too = fgetc(F) << 24 // shift first byte to left of int | fgetc(F) << 16 // shift second byte to 2nd from left | fgetc(F) << 8 // shift 3rd byte to 2nd from right | fgetc(F); // leave 4th byte where it is at far right return (int)x; // return as a signed integer } /* PUT AN INT TO A 4-BYTE TRAIN IN A FILE: NO FLUSH. Called from a large number of places throughout the program */ void putInt(int X, FILE *F) { unsigned int x = (unsigned int)X; // need to shift the sign bit too fputc(x >> 24 & 0xFF, F); // Store the new number of records fputc(x >> 16 & 0xFF, F); // as a 4 byte int in the first fputc(x >> 8 & 0xFF, F); // 4 bytes of the records file. fputc(x & 0xFF, F); } /* GET A TWO-BYTE [16-BIT] QUANTITY FROM DISK FILE. Note that the retrieved quantity is unsigned but it is placed into a signed 32-bit integer. */ int getShort(FILE *F) { return fgetc(F) << 8 | fgetc(F); } /* PUT A TWO-BYTE [16-BIT] QUANTITY TO DISK FILE. Note that the signed integer 'x' is stored as an unsigned 16-bit quantity in the range +0 to +0xFFFF [+65535]. */ void putShort(int x, FILE *F) { fputc(x >> 8 & 0xFF,F); fputc(x & 0xFF,F); } /* CONVERT AN 'int x' TO AN l-DIGIT NUMERIC STRING WITH LEADING SPACES OR ZEROS IN ARRAY S[].*/ char *showInt(int x, int l, int y) { int f = 0; // indicates that the presented number 'x' is positive static char S[33]; // to hold the string version of the number char s = ' '; // for leading spaces if(y) s = '0'; // for leading zeros if(l > 11) l = 11; // safety precaution for(int i = 0; i < l; i++) S[i] = s; // fill the whole field with spaces or zeros S[l - 1] = '0'; // the least-significant char must be a '0' S[l] = (char)0; // terminating NULL character if(x < 0) { // if the presented number is negative x = -x; // reverse its sign f = 1; // indicate that 'x' as presented was negative } if(x > 0) { // if the number is non-zero: int i = l; // total of 'l' possible digits in the number /* While there're still more digits to process, work back- wards from [8] to [0], storing the remainder after dividing the number by 10 as a numeric character in array S[]. Then divide the number by 10 [not rounded] and loop back. */ while(x > 0 && i > 0) { S[--i] = (char)(x % 10 + 48); x /= 10; } if(f == 1) // if x was negative, S[0] = '-'; // make the first character a minus sign } return S; // return the address of start of char array } // containing the formatted number. /* CHECK THAT THE YEAR, MONTH AND DAY OF A DATE ARE WITHIN VALID RANGE Called one by endDate(), and twice by getET(). */ int monthCheck(int y, int m, int d) { int n = dm[m]; // get the number of days in the given month // if it's February, add an extra day if 'y' is a leap year if(m == 2 && (y % 4 == 0 && y % 100 != 0 || y % 400 == 0)) n++; // check that the year, month and day numbers are within their valid ranges if(y < 999 || y > 9999 || m < 1 || m > 12 || d < 1 || d > n) return 1; return 0; } /* GET THE NUMBER OF DAYS IN THE GIVEN MONTH [INCLUDING THE LEAP YEAR TEST if it's February] AND { (the year is divisible by 4 but not by 100) OR (it is divisible by 400) } then it is a leap year so add an extra day. Called twice from endDate() and once from getET(). */ int daysInMonth(int y, int m) { int n = dm[m]; /* get the number of days in the given month if the month is February | and the year divides exactly by 4 | | but does not divide exactly by 100 | | | | | | */ if(m == 2 && (y % 4 == 0 && y % 100 != 0 || y % 400 == 0)) n++; /* | | OR it divides exactly by 400 | then it's a LEAP YEAR, so add an extra day to February */ return n; // return the correct number of days for the month } /* COMPUTE THE END DATE THAT IS A GIVEN NUMBER OF DAYS AFTER A GIVEN START DATE. Called from 1 place in T5crops(). start date number of days the end date is into the future | | */ int endDate(int s, int t) { if(t < 0 || t > 1000) return 0; // exit if elapsed time is out of range int x = s % 10000, // the month and day numbers as a single integer y = s / 10000, // the year number as an integer m = x / 100, // month number as an integer d = x % 100; // day number as an integer if(monthCheck(y,m,d)) return 0; // check integrity of the given date int n = daysInMonth(y,m); // get number of days in this month while(t > 1) { // while there are still more extra days left: if(++d > n) { // If on incrementing the day number it overshoots the d = 1; // number of days in the current month, reset it to 1 if(m++ > 12) { // if incremented month is now beyond 12 [December] m = 1; // set the month to 1 [January] y++; // advance to the new year } n = daysInMonth(y,m); } t--; // decrement the number of days to go } return y * 10000 + m * 100 + d; // return the rationalised end date } /* COMPUTE THE ELAPSED TIME [IN DAYS] BETWEEN TWO DATES. Called from 1 place in T6showTrans() and 2 places in T5crops(). start date end date | | */ int getET(int D1, int D2) { if(D1 == D2) return 0; // if dates are the same /* If the 'end' date is before the 'start' date, then swap the dates over and set the negative flag 'f' */ int f = 0, x; if(D1 > D2) {x = D1; D1 = D2; D2 = x; f = 1; } /* unpack the start date 'D1' into year 'y', month 'm' and day 'd' and the end date 'D2' into year 'Y', month 'M' and day 'D' */ x = D1 % 10000; int y = D2 / 10000, m = x / 100, d = x % 100; x = D2 % 10000; int Y = D2 / 10000, M = x / 100, D = x % 100; // check that the years, months and days are within valid range if((monthCheck(y,m,d)) || (monthCheck(Y,M,D))) return 0; // invalid int t = -d; // set elapsed time initially to -(start date's day number) while(1) { // permanent but breakable loop /* If the 'start' month = the 'end' month and the 'start' year = the 'end' year, the elapsed time is the difference between day numbers; so add the number of days to date in the 'end' month and exit. */ if(m == M && y == Y) { t += D; break; } t += daysInMonth(y,m-1); // add the number of days in this month /* if, on incrementing the month number, it is now beyond month 12 [Dec- ember], reset it to month 1 [January] and increment the year number. */ if(++m > 12) { m = 1; y++; } /* if the elapsed time goes beyond 10000 days, set it to zero and break out of the the while() loop. */ if(t > 9999) { t = 0; break; } } if(f == 1) t = -t; // if start date is future, invert sign of elapsed time return t; // return the elapsed time } /* SWAP A PAIR OF RECORDS OF THE INDEX SECTIONS OF THE 'nad.dat', 'diary.dat', 'crop.dat' or 'fin.dat' FILES. start byte of first record | start byte of second record | | record length | | | */ void sortSwap(int i, int j, int K) { char I[K], // to hold content of i-th record J[K]; // to hold content of j-th record int k; fseek(FS,i,SEEK_SET); // start byte of the i-th record for(k = 0; k < K; ++k) // put content of i-th record into array I[] I[k] = fgetc(FS); fseek(FS,j,SEEK_SET); // start byte of the j-th record for(k = 0; k < K; ++k) // put content of j-th record into array J[] J[k] = fgetc(FS); fseek(FS,i,SEEK_SET); // start byte of the i-th record for(k = 0; k < K; ++k) // put content of J[] into i-th record fputc(J[k],FS); fseek(FS,j,SEEK_SET); // start byte of the j-th record for(k = 0; k < K; ++k) // put content of I[] into j-th record fputc(I[k],FS); } /* COMPARE THE CONTENT OF INDEX RECORD 'i0' WITH THAT OF A REFERENCE TERM 'r' Compare each character of the reference term in st[] with each corresponding character of the element retrieved from the disk record. The first pair of characters that are unequal completes the test. If the reference-term is greater than the retrieved element, return a value < 0. If the retrieved element is greater than the reference-term, return a value > 0. Otherwise, if the elements are equal, return 0. Called twice from idxSort(), 1 place each in T0slice(), T0step() and T0firstExact() and 2 places in T0FHNM(). */ int strComp(int i) { // i = record number of the current index record /* file handle for the current index file | record number of current index record [range: 1 to N0 inclusive] | | multiply by 16 to get its current start byte | | | add a further 2 to get start byte of index string | | | | */ fseek(FS,(i << 4) + 2,SEEK_SET); // seek start byte of index string for(int i = 0; i < 14; i++) { // for each of the 14 possible characters int r = st[i], // get next char from the reference string x = fgetc(FS); /* get the next one from the index file if the current character of the element retrieved from the index file | is below | | the corresponding character of the reference element in array st[] | | | */ if(x < r) return -1; /* then return a value less than zero if the current character of the element retrieved from the index file | is above the | | the corresponding character of the reference element in array st[] | | | */ if(x > r) return +1; /* then return a value greater than zero If we've reached here then both the current character of retrieved element and the corresponding character of the reference element must be the same, so if either is zero, we've finished and can return immediately. */ if(x == 0) return 0; // returning zero says that both terms are equal } return 0; /* If we finish the entire loop, it means that all 30 characters of the retrieved element and the reference element are the same, so we exit with zero to indicate that they're identical. */ } /* SORTS AN INDEX WITHIN 'xxx.dat' INTO ALPHABETICAL [OR RATHER, ASCII CASTING] ORDER. This function is based on the Hoare Quick-Sort. It works with an index in the index section of the file whose handle is within the global file-pointer variable 'FS'. Called from 2 places within itself and from from 1 place in T0regen(), T0diarySort(), T5cropSort() and T6transSort(). lowest occupied element of the index to be sorted | highest occupied element of the index to be sorted | | */ void idxSort(int L, int H) { int l = L, // set moving low to LOW end of partition h = H; // set moving high to HIGH end of partition if(H > L) { // if partition contains anything: /* PUT THE CONTENT OF THE MIDWAY RECORD INTO st[] file handle of the index file being sorted 'xxx.dat' | lowest element of the sort range | | highest element of the sort range | | | divide by 2 to get the midway element | | | | multiply by 16 to get the element's start byte | | | | | add 2 to skip record number & field length | | | | | | */ fseek(FS,((L + H >> 1) << 4) + 2,SEEK_SET); for(int i = 0; i < 14; i++) // copy the 14 byte text field st[i] = fgetc(FS); // into the 'sort term' array st[] while(l <= h) { /* loop through file until the indexes cross while the current low element | is below | | the upper boundary of the current partition | | | AND the content of the lower element | | | | | is less than | | | | | | the content of the partition's mid-point | | | | | | | move the 'low element' up by one place | | | | | | | | */ while(l < H && strComp(l) < 0) l++; /* while the current high element | is above | | the lower boundary of the current partition | | | AND the content of the high element | | | | | is greater than | | | | | | the content of the partition's mid-point | | | | | | | move the 'high element' down by one place | | | | | | | | */ while(h > L && strComp(h) > 0) h--; if(l <= h) { /* IF THE LOW ELEMENT IS AT OR BELOW THE LEVEL OF THE HIGH ELEMENT: number of the moving 'low' element | multiply by 16 [the number of bytes per record] | | number of the moving 'high' element | | | multiply by 16 [the number of bytes per record] | | | | | | | | THEN SWAP THE CONTENTS OF */ sortSwap(l << 4,h << 4,16); // THE HIGHER & LOWER ELEMENTS. l++; // push the lower sort boundary up by one element h--; // pull the upper sort boundary down by one element } } if(L < h) idxSort(L,h); // sort the lower partition if(l < H) idxSort(l,H); // sort the upper partition } } /* DISPLAY ADVICE AND ERROR MESSAGES. Called from a large number of places throughout the program. */ void showMsg(int m) { const int clr[] = { DARK,GREEN,YELLOW,YELLOW,RED,RED,YELLOW,GREEN,RED,RED,YELLOW,RED, GREEN,GREEN,GREEN,GREEN,YELLOW,YELLOW,YELLOW,YELLOW,YELLOW,RED, GREEN,GREEN,YELLOW,GREEN,GREEN,GREEN,YELLOW }; const char msg[] = { 0,18,27,31,32,32,37,56,31,53,14,36,18,18,19, 27,53,41,24,28,30,24,20,41,40,25,15,14,62}; const char *MSG[] = { "", "Exact match found.", "Nearest higher match found.", "Nearest lower match only found.", "Search term beyond end of index.", "No more spare records available.", "Trawling for the entered search term.", "Search term found. Click NEXT button to continue search.", "Search term could not be found.", "Couldn't find this record in the index. See terminal.", "RECORD DELETED", "INDEX INTEGRITY ERROR. See terminal.", "INDEX INTEGRITY OK", "DATABASE WAS RESET", "INDEXES REGENERATED", "Landshare data regenerated.", "This field's content is computed: it can't be edited.", "The requested crop record does not exist.", "There is no next record.", "There is no previous record.", "Re-sorting the selected index.", "No diary entry selected.", "Diary entry deleted.", "The edited field content has been stored.", "Editing of the field has been cancelled.", "Transaction record found.", "Data backed up.", "Data Restored.", "This function is not implemented in the demonstration version." }; if(m == 0) { XSetForeground(XD,XG,GREY); XDrawString(XD,XW,XG,17,345,"MESSAGE",7); } XSetForeground(XD,XG,DARK); // clear the message area XFillRectangle(XD,XW,XG,65,331,418,21); XSetForeground(XD,XG,clr[m]); XDrawString(XD,XW,XG,71,345,MSG[m],msg[m]); } /* IMPLEMENTS LIST SCROLLING WITH THE MOUSE WHEEL AND DISPLAYS THE SCROLL BAR Called from T0listScroll(), T0diaryScroll(), T5cropScroll(), T6transList(). position in list of first displayed line before scrolling | current number of items in the full list [file] | | */ int Scroller(int g, int s) { /* MOUSE CONTROL FOR LIST SCROLLING just scrolled down the list by one position | current offset of start of display from record 1 of file | | current number of items in the whole list [file] | | | number of items at a time displayed on screen | | | | */ if(mw == 1 && g < s - 11) g++; // if Mouse Wheel 1 scroll down the list else if(mw == 2 && g > 0) g--; // if Mouse Wheel 2 scroll back up list mw = 0; // Mouse wheel event now dealt with // IF MORE THAN 11 LINES IN THE FILE WE NEED TO DISPLAY A SCROLL BAR if(s > 11) { /* if there are more than '11' records in the file total height of the scroll-bar area: 209 pixels | times '11' data lines on the screen | | current number of occupied records | | | */ int h = 209 * 11 / s; /* height of highlighted part of the scroll bar height of the dull part of the scroll area above the highlighted part | number of the first record in the list | | total height of the scroll bar area | | | height of highlighted part of the scroll bar | | | | current number of occupied records | | | | | number of lines in the displayed list | | | | | | */ int v = g * (209 - h) / (s - 11); XSetForeground(XD,XG,DARK); // show full length scroll bar in grey XFillRectangle(XD,XW,XG,475,84,8,209); XSetForeground(XD,XG,PREEN); // show highlighted part of scroll bar XFillRectangle(XD,XW,XG,475,84+v,8,h); } return g; /* returns the position within the whole list [file] of the first displayed line after scrolling */ } /* HANDLES A CLICK ON ONE OF THE LIST ITEMS. Called from T0listClick(), T0diaryClick(), T5cropListClick(), T6transListClick(). */ int listClick() { int i = 0; // loop variable if(mx > 16 && mx < 483) { // if click within width of a list data line int a = 85, // top of the clickable area of the top data line b = 103; // botton of the clickable area of the top data line for(i = 1; i < 12; i++) { // for each of the 11 data lines if(my > a && my < b) { // if click in this line's vertical limits break; // set number of selected line; break loop } a += 19; b += 19; // otherwise drop to the next line down } // NOTE: clicking anywhere else on the } // sub-tab de-selects all lines. if(i > 0 && i < 12) // provided click was on one of the 11 lines return i; // return the number of the clicked line return 0; // return that click was not on a list item } // the highlighed list item // FUNCTIONS PERTAINING TO TAB 6: TRADE ------------------------------------- /* DRAW HORIZONTAL ROW OF CONTROL BUTTONS ACROSS BOTTOM OF TRADE TAB Called from only one place in T6show() and T6click(). */ void T6showHorizButs(){ showButs(); // show 8 blank buttons across the bottom of the tab area const char // annotate the horizontal row of control buttons *A[] = {"CONSU","FABRI","ARTS","CRAFTS","SPARE","SPARE","TAX","HELP"}; const int a[] = {12,12,15, 9,12,12,18,15}, // horizontal offsets for word starts b[] = { 5, 5, 4, 6, 5, 5, 3, 4}; // number of letters in each word int h = 17; // x coordinate of button block for(int i = 0; i < 8; i++) { // for each of the 8 buttons: if(a6 & 1 << i) // if this button is bright [on] XSetForeground(XD,XG,WHITE); // set colour for bright lettering else XSetForeground(XD,XG,GREY); // set colour for dull lettering XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]); h += 59; // move across to the next button } } /* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE RAIN SUB-TAB. Called from T2showGraph(), T2showRainGraph(), T2showRainTank() */ void T6showVertiButs() { char // annotate the vertical row of control buttons *A[] = {"FIND","NEXT","PREV","EDIT","NEW","LIST","STATUS","HELP"}; int a[] = {15,15,15,15,18,15, 9,15}, // horizontal offsets for word starts b[] = { 4, 4, 4, 4, 3, 4, 6, 4}, // number of letters in each word h = 99; // y coordinate of button block for(int i = 0; i < 8; i++) { // for each of the 8 buttons: XSetForeground(XD,XG,DARK); // shade for buttons' background XFillRectangle(XD,XW,XG,17,h-15,53,21); if(b6 & 1 << i) // if this button is bright [on] XSetForeground(XD,XG,WHITE); // set colour for bright lettering else // otherwise XSetForeground(XD,XG,GREY); // set colour for dull lettering XDrawString(XD,XW,XG,17+a[i],h,A[i],b[i]); h += 27; // move across to the next button } } /* TAB 6 EXPOSURE FUNCTION. Called only from 1 place in showTab(). */ void T6status() { if(tb != 6) return; // bail out if TRADE tab not currently visible int h = 87, // horizontal start coord of text line v = 117, // vertical start coord of top text line l = 20, // inter-line drop i = 0, // loop variable I = 9, // loop size D[] = {16573, // total value to date of sales invoices 13298, // total value to date of purchase bills 0, // profit [calculated herein] 8000, // value of work in progress 20000, // current account balance 1150, // current outstanding debt 15290, // current outstanding credit 1000, // average spending per day 0}, // survival time in days [calculated herein] d[] = {5,5,5,5,5,5,5,5,3}, // data field lengths s[] = {61,61,51,54,44,45,47,45,60}; // text field lengths char *S[] ={ // annotation text lines "A SALES + value of sales invoices so far this year.", "B PURCH - value of purchase bills so far this year.", "C PROFIT + profit to date this year [A-B].", "D WIP current value of work in progress.", "E BALANCE + current account balance.", "F DEBT - current outstanding debt.", "G CREDIT - current outstanding credit.", "H OUTGOINGS - average spending per day.", "I SURVIVAL days survival time on zero income [(E-F)/H].", }; XSetForeground(XD,XG,BLACK); // show column headings in yellow XFillRectangle(XD,XW,XG,17,53,466,270); XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"TRADE STATUS SUMMARY",20); XDrawString(XD,XW,XG,h,95,"VALUE: " "some may still be awaiting materialisation as cash.",63); XDrawString(XD,XW,XG,h,207,"CASH: " "in possession [some may be still owed to suppliers].",64); XSetForeground(XD,XG,BLEEN); // colour for the lettering for(i = 0; i < I; i++) { // for each data annotation text line XDrawString(XD,XW,XG,h,v,S[i],s[i]); if(i == 3) v += 32; // jump down to first line of CASH data v += l; // drop down to next data line } D[2] = D[0] - D[1]; // profit to date this year D[8] = (D[4] - D[5]) / D[7]; // survival time on zero future income h = 158; // horizontal start coord of data fields v = 117; // vertical start coord of top data field XSetForeground(XD,XG,WHITE); // colour for the lettering for(i = 0; i < I; i++) { // for each data item int x = d[i]; // length of data field XDrawString(XD,XW,XG,h,v,showInt(D[i],x,1),x); if(i == 3) v += 32; // jump down to first line of CASH data v += l; // drop down to next data line } a6 = 1; // set first sub-tab button bright T6showVertiButs(); // re-display the vertical control buttons showMsg(0); // display blank message field } /* GET THE NUMBER OF DAYS THAT HAS ELAPSED SINCE THE INVOICE DATE OR THE NUMBER OF DAYS FROM INVOICE TO PAYMENT. The file pointer [handle] FD for 'fin.dat' must have been pre-set to point to the invoice's DATE field. Called from 1 place each in T6showTrans(), T6transList() and T6transSort(). */ int T6getDays(int B) { // B=start byte of of 32-bit invoice record int ET = 0; // set number of days before payment deadline to zero fseek(FD,B,SEEK_SET); // seek start byte of invoice DATE field int id = getInt(FD); // get INVOICE date fseek(FD,B + 24,SEEK_SET); // seek start byte of payment DATE field int pd = getInt(FD); // get PAID date C6 = WHITE; // set default display colour to white if(id == 0) { // if no invoice date exists [invoice date is zero] pd = 0; // set the payment date to zero return 0; // exit with the elapsed time as zero } /* Otherwise, an invoice date has been specified, so the invoice is either still unpaid or it has already been paid. If the 'date PAID', as specified in the transaction record is zero, it means that the invoice is still with- in the payment deadline but has not yet been paid. */ if(pd == 0) { // if the payment has still not been received pd = ratSysDate(); // set the 'DATE PAID' to the current system date ET = getET(id,pd); // compute the elapsed time from invoice date to now if(ET < 0) // if the elapsed time is negative, it means that the C6 = GREEN; // payment was made in advance, so show it in GREEN else if(ET < 30) // if the elapsed time still less than 30 days C6 = AMBER; // show the anticipated payment date in AMBER else { // else the payment of this invoice is overdue C6 = RED; // so show the anticipated payment date in RED V6 = 1; // and set the 'payment alert' flag } pd = endDate(id,30); // compute the anticipated payment date } /* Otherwise, the payment has already been received, so return the time between the invoice date and the date it was paid. */ else ET = getET(id,pd); return ET; } /* DISPLAY THE SCROLLABLE INVOICE LIST. Called only from one place each in T6transList() and T6transUpDn(). fin.dat record format: 32-byte records 0000 transaction date 0000 ref number of client in 'people.dat' 0000 ref number of product in 'crop.dat' 0000 price 0000 quantity 0000 value 0000 date paid 0000 spare */ void T6transScroll() { int v = 93, // vertical coordinate of field i; // main loop variable XSetForeground(XD,XG,BLACK); // clear the list area XFillRectangle(XD,XW,XG,86,75,466,220); g6 = Scroller(g6,N6); /* adjust scroll off-set if necessary START BYTE OF THE FIRST DATA RECORD TO BE DISPLAYED | scroll off-set of records before the first on to be displayed | | | times the number of bytes per record | | | | */ int B = g6 + 1 << 5; int o = o6 - 1; // one less because the DOCument number is not indexed /* START BYTE OF THE FIRST INDEX RECORD TO BE DISPLAYED | | Size of the transactions data section of 'fin.dat' | | in equivalent 16-byte records. | | | | index number of the selected list column | | | times 256 16-bytes records per index | | | | | | | | Scroll off-set: the number of index records | | | | | before the first one to be displayed. | | | | | | | | | | +1 more to get to the 1st displayed line | | | | | | all times 16 bytes per index record | | | | | | | */ int I = 0x200 + (o << 8) + g6 + 1 << 4; int R; // data record number within 'fin.dat' for(i = 1; i < 12; i++) { // for each of the next 11 records if(o6 > 0) { // if displaying in index order fseek(FD,I,SEEK_SET); // seek start byte of [next] index record R = fgetc(FD); // number of required main data record B = R << 5; // start byte of the required main record } fseek(FD,B,SEEK_SET); // seek start byte of main data record int a = getInt(FD), // transaction date b = getInt(FD), // transaction type c = getInt(FD), // ref number of client in 'people.dat' d = getInt(FD), // ref number of product in 'crop.dat' e = getInt(FD), // price f = getInt(FD), // quantity g = getInt(FD), // date paid h = g6 + i; // document number for listing in document order if(o6 > 0) h = R; /* document number when listing in indexed order default colour for a line within the transaction list | index record number of currently highlighted list item | | | | Number of lines in the data or index file prior | | | to the first one currently being displayed. | | | | | | Line number of highlighted line within the | | | | 11 that are currently visible on screen. | | | | | | | | */ int C = BLEEN; if(l6 == g6 + i) { C = WHITE; r6 = h; } /* | | | colour for a highlighted line | | record number of current data record | document number of line being displayed */ XSetForeground(XD,XG,C); // colour for the data in the list XDrawString(XD,XW,XG,89,v,showInt(h,3,1),3); // DOC number XDrawString(XD,XW,XG,113,v,showInt(a,8,1),8); /* invoice DATE file handle for 'nad.dat', which contains the client names | get the client number from the transaction record | | times 1024 bytes per record | | | plus offset of client's short name field | | | | */ fseek(FA,(c << 10) + 409,SEEK_SET); // seek short CLIENT NAME char S[13]; // to hold client or product name int K = fgetc(FA), k; // get the 1st byte of the short CLIENT NAME if(K < 0) K = 0; // invalid line length if(K > 12) K = 12; // upper safety limit for(k = 0; k < K; k++) // for each character of the CLIENT NAME S[k] = fgetc(FA); // get the byte from the disk record XDrawString(XD,XW,XG,165,v,S,K); /* display the CLIENT NAME file handle for 'crop.dat', which contains the CROP NAME | get the crop type number from the crop record | | times 64 bytes per crop record | | | */ fseek(FC,d << 6,SEEK_SET); // start byte of crop record K = fgetc(FC); // get the first byte of current record if(K < 0) K = 0; // invalid line length if(K > 13) K = 13; // upper safety limit for(k = 0; k < K; k++) // for each character of the product name S[k] = fgetc(FC); // get it from the disk record XDrawString(XD,XW,XG,243,v,S,K); // display the CROP NAME XDrawString(XD,XW,XG,327,v,showInt(e,5,1),5); // show price XDrawString(XD,XW,XG,363,v,showInt(f,5,1),5); // show quantity XDrawString(XD,XW,XG,402,v,showInt(e * f,5,1),5); // show value int ET = T6getDays(B); // get the elapsed time and its display colour XSetForeground(XD,XG,C6); // set display colour for the days to run XDrawString(XD,XW,XG,443,v,showInt(ET,4,1),4); v += 20; // drop down to the next line to be displayed if(o6 == 0) // if displaying in order of document number B += 32; // advance to the start byte of the next data record else // else, we're displaying in the order of a column index I += 16; // advance to start byte of next index record } } /* Colour of the whole line: RED=unpaid, YELLOW = due, GREEN = paid [settled The column space at the left is for the scroll bar. */ void T6transList() { XSetForeground(XD,XG,BLACK); // clear the list area of the tab XFillRectangle(XD,XW,XG,17,53,466,270); // DISPLAY THE COLUMN HEADING BUTTONS XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"INVOICES:",9); XSetForeground(XD,XG,DARK); // colour for the title buttons XFillRectangle(XD,XW,XG, 86,56,22,19); // DOC button XFillRectangle(XD,XW,XG,112,56,49,19); // DATE button XFillRectangle(XD,XW,XG,163,56,75,19); // NAME button XFillRectangle(XD,XW,XG,241,56,81,19); // PRODUCT button XFillRectangle(XD,XW,XG,325,56,33,19); // PRICE button XFillRectangle(XD,XW,XG,361,56,33,19); // QUANT button XFillRectangle(XD,XW,XG,397,56,39,19); // COST button XFillRectangle(XD,XW,XG,439,56,33,19); // PAID button char *S[] = {"DOC","DATE","NAME","PRODUCT","PRICE","QUANT","VALUE","PAID?"}; int s[] = {3,4,4,7,5,5,5,5}; int n[] = {89,124,189,261,327,363,402,441}; int C; for(int i = 0; i < 8; i++) { if(b6 & 0x100 << i) C = WHITE; else C = GREY; XSetForeground(XD,XG,C); // colour for the lettering XDrawString(XD,XW,XG,n[i],70,S[i],s[i]); } XSetForeground(XD,XG,BLEEN); // colour for the lettering XDrawLine(XD,XW,XG,384,301,430,301); XDrawString(XD,XW,XG, 87,317,"TOTAL",5); int B = 48, i, // start byte of PRICE field of Record 1 T = 0; // total value of all invoices for(i = 1; i < N6; i++) { // sum all the costs fseek(FD,B,SEEK_SET); // start byte of price field T += getInt(FD) * getInt(FD); // times the value of the sale or purchase B += 32; // advance to next record } XDrawString(XD,XW,XG,384,317,showInt(T,8,1),8); T6showVertiButs(); showMsg(0); T6transScroll(); } /* CLEARS THE [sets to data-box background colour] the INSIDES OF THE DATA BOXES OF THE TRADE->TRANS SUB-TAB. Called from T6clearInsides() and T6transNEW(), */ void T6clearInsides() { XSetForeground(XD,XG,DARK); // colour for the display field background int d, v = Y6 - 12, x = X6 + 54; for(int i = 0; i < 10; i++) { XFillRectangle(XD,XW,XG,X6-3,v,53,15); // paint background if(i == 2) // if it's a document type XFillRectangle(XD,XW,XG,x,v,101,15); // paint background 16 chars wide else if(i > 2 && i < 5) // if it's a trader or product XFillRectangle(XD,XW,XG,x,v,202,15); // paint background 32 chars wide v += 20; } } /* FETCH AND DISPLAY A FINANCIAL TRANSACTION RECORD [SALES OR PURCHASE INVOICE OR CREDIT NOTE. Called from T6show(), T6horizButClick(). */ void T6showTrans() { char // customer/supplier name in identifier order *S[] = { // field names "DOCUMENT NUMBER.","DATE OF ISSUE...","TRANSACTION TYPE", "TRADER IDENTITY ","PRODUCT TYPE....","PRICE PER UNIT..", "QUANTITY........","COST [TOTAL]....","PAID?...........", "DELAY..........." }, *T[] = { // transaction type in numbered order "CANCELLED ", "SALES INVOICE ", "PURCHASE INVOICE", "SALES CREDIT ", "PURCHASE CREDIT " }, D[32]; clearTab(); // clear the tab area T6clearInsides(); // clear the insides of all data boxes XSetForeground(XD,XG,YELLOW); // colour for the title lettering XDrawString(XD,XW,XG,17,70, "FINANCIAL TRANSACTION [SALES/PURCHASE INVOICE OR CREDIT NOTE]",61); // DISPLAY THE FIELD NAMES OF THE TRADE DOCUMENT XSetForeground(XD,XG,BLEEN); int X = 87, // horizontal start position of the data block l = 20, // inter-line spacing within data block y = Y6; // vertical coordinate of current data line for(int i = 0; i < 10; i++) { // for each data value XDrawString(XD,XW,XG,X,y,S[i],16); y += l; // drop down to next data line } // DISPLAY SUNDRY ANNOTATIONS X = X6 + 56; y = Y6 + (l << 2) + l; XDrawString(XD,XW,XG,X,y,"VALS/KG",7); XDrawString(XD,XW,XG,X,y + l,"KG",2); XDrawString(XD,XW,XG,X,y + l + l,"VALS",4); XDrawString(XD,XW,XG,X,y + (l << 2),"DAYS",4); XDrawString(XD,XW,XG,17,316,"Note: tax can only be levied on" " productive gain: never on sales or purchases.",77); // GET AND DISPLAY THE TRANSACTION RECORD DATA y = Y6 - l; // vertical coordinate of top line of data block XSetForeground(XD,XG,WHITE); // default colour for display of data items int B = r6 << 5; // start byte of transaction data record fseek(FD,B,SEEK_SET); // start byte of record 'r6' int a = getInt(FD), // trans date b = getInt(FD), // trans type c = getInt(FD), // trader number type d = getInt(FD), // crop type number e = getInt(FD), // price f = getInt(FD), // quantity g = getInt(FD), // date paid ET = T6getDays(B); // elapsed time from invoice date XDrawString(XD,XW,XG,X6,y+=l,showInt(r6,8,0),8); // document number XDrawString(XD,XW,XG,X6,y+=l,showInt(a,8,0),8); // transaction date XDrawString(XD,XW,XG,X6,y+=l,showInt(b,8,0),8); // trans type XDrawString(XD,XW,XG,X6,y+=l,showInt(c,8,0),8); // trader num XDrawString(XD,XW,XG,X6,y+=l,showInt(d,8,0),8); // crop num XDrawString(XD,XW,XG,X6,y+=l,showInt(e,8,0),8); // price XDrawString(XD,XW,XG,X6,y+=l,showInt(f,8,0),8); // quantity XDrawString(XD,XW,XG,X6,y+=l,showInt(e * f,8,0),8); // total cost XDrawString(XD,XW,XG,X6,y+=l,showInt(g,8,0),8); // payment date XSetForeground(XD,XG,C6); // show ELAPSED TIME in appropriate colour XDrawString(XD,XW,XG,X6,y+=l,showInt(ET,8,0),8); XSetForeground(XD,XG,WHITE); // colour for the names on the right y = Y6 + l; // reset to y-coord of text fields if(b > -1 && b < 5) // if record contains a valid transaction type number XDrawString(XD,XW,XG,X,y+=l,T[b],16); // display TRANSACTION TYPE NAME if(c > 0 && c < 256) { // if Trader Ref Num is within valid range fseek(FA,c << 10,SEEK_SET); // first byte of trader's NAD record int I = fgetc(FA); // get length of TRADER'S NAME if(I < 0) I = 0; // make length zero if negative [corrupt] if(I > 32) I = 32; // limit the name length to 32 characters for(int i = 0; i < I; i++) // for each character in TRADER'S NAME D[i] = fgetc(FA); // put character in character array XDrawString(XD,XW,XG,X,y+=l,D,I); } if(d > 0 && d < N5) { // if 'e' is a valid crop record number fseek(FC,d << 6,SEEK_SET); // start byte of crop record int I = fgetc(FC); // length of CROP NAME if(I < 0) I = 0; // make length zero if negative [corrupt] if(I > 32) I = 32; // limit the name length to 32 characters for(int i = 0; i < I; i++) // for each character in CROP NAME D[i] = fgetc(FC); // put character in character array XDrawString(XD,XW,XG,X,y+=l,D,I); } XSetForeground(XD,XG,GREY); // End note X += 60; y = Y6 + 100; y += 20; XDrawString(XD,XW,XG,X,y,"GREEN: paid within 30 days.",27); y += 16; XDrawString(XD,XW,XG,X,y,"AMBER: unpaid but is still ",27); y += 16; XDrawString(XD,XW,XG,X,y,"within the 30 day deadline,",27); y += 16; XDrawString(XD,XW,XG,X,y,"or paid but paid late. RED:",27); y += 16; XDrawString(XD,XW,XG,X,y,"unpaid beyond 30 day limit.",27); T6showVertiButs(); T6showHorizButs(); showMsg(0); } void T6showSpare() { char *S[] = { "The TRADE tab of this program facilitates only the mode of trade of a", "future community-based world of Landshare units, using a non-infating", "non-interest-attracting unit of exchange called the 'Val'. ", "Notwithstanding, for at least a considerable time, Landshare communi-", "ties will have to operate within present-day Capitalist and Socialist", "jurisdictions. For this reason, each trading coterie, within the con-", "federation of Landshare communities, must become a Limited Liability ", "entity within the present-world jurisdiction in which it is located. ", "It is therefore necessary for each Landshare unit to have a financial", "interface with its respective present-world jurisdiction. Such is im-", "plemented within these two SPARE sub-tabs to suit the requirements of", "the jurisdiction in which each particular application site lies. " }; int y = 80; XSetForeground(XD,XG,BLEEN); for(int i = 0; i < 12; i++) { if(i == 3 || i == 8) y+=10; XDrawString(XD,XW,XG,40,y+=19,S[i],69); } } void T6showSubTab1() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"SUB-TAB 1",9); char *S[] = { "The FABRI sub-tab has exactly the same functionality as the CONSU sub", "tab but records the trade of fabricated goods instead of consumables.", "However, it must be accounted in a different currency from the Val. ", "This is because, in Landshare World, fabricated items are not regard-", "ed as consumable. Neither are their expendable components. Fabricated", "items are regarded as permanent. Their expendable components are not ", "produced on an on-going consumer basis but instead they are made only", "as and when they are needed, using a process such as 3-D printing. ", "This removes the motive to fabricate products with a limited life and", "uses vastly less materials and energy in the long term, which is much", "better for the conservation and health of the planet's biosphere. " }; int y = 80; XSetForeground(XD,XG,BLEEN); for(int i = 0; i < 11; i++) { if(i == 3 || i == 8) y+=10; XDrawString(XD,XW,XG,40,y+=19,S[i],69); } } void T6showSubTab2() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"SUB-TAB 2",9); char *S[] = { "Likewise the ARTS sub-tab has the same functionality as the CONSU sub", "tab but accounts the exchange of works of art instead of consumables.", "The ARTS sub-tab must work in yet another currency of exchange. ", "This is because, in Landshare World, works of art too aren't regarded", "as consumable. Each work or art has a unique and permanent existence.", "Its creator is not dependent on the sale of his works of art in order", "to live and survive economically. It is a passtime activity motivated", "by his creativity: not by any lust for gain. ", "This raises the issue of whether or not works of art should be traded", "in a currency at all. Can they not be simply created for the love of ", "the art and given to others as presents? " }; int y = 80; XSetForeground(XD,XG,BLEEN); for(int i = 0; i < 11; i++) { if(i == 3 || i == 8) y+=10; XDrawString(XD,XW,XG,40,y+=19,S[i],69); } } void T6showSubTab3() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"SUB-TAB 3",9); char *S[] = { "Likewise also, the CRAFTS sub-tab contains the same functionality as ", "the CONSU sub-tab but exchanges its goods for yet a 4th type of curr-", "ency. Crafts include the production of goods like clothes and shoes. ", "Unlike food and fuel, which are of the same form for everybody, and ", "are consumed at a steady rate, crafted items, like shoes and clothes,", "should be bespokely crafted for each individual in each instance. The", "present obsession with standardization in production is all too pain-", "ful with the impossibility of finding a pair of trowsers that fit. ", "I have never been able to understand why it is not blindingly obvious", "that steady-state on-going consumer economics is systemically incomp-", "atible with permanent items and those requiring individualisation. " }; int y = 80; XSetForeground(XD,XG,BLEEN); for(int i = 0; i < 11; i++) { if(i == 3 || i == 8) y+=10; XDrawString(XD,XW,XG,40,y+=19,S[i],69); } } void T6showSubTab4() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"SUB-TAB 4",9); T6showSpare(); } void T6showSubTab5() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"SUB-TAB 5",9); T6showSpare(); } void T6showSubTab6() { XSetForeground(XD,XG,WHITE); // colour for the lettering XDrawString(XD,XW,XG,17, 70," DUE PAID ",35); XDrawString(XD,XW,XG,17, 90,"TAXES ON PROFIT 00000.00 00000.00",35); XDrawString(XD,XW,XG,17,110,"VALUE ADDED TAX 00000.00 00000.00",35); } /* SHOW TAB 6 [TRADE] ON INITIAL EXPOSURE AND WHEN RE-EXPOSURE EVENTS OCCUR Called only from showTab(). */ void T6show() { if(tb != 6) return; // bail out if TRADE tab not currently visible if(a6 & 1) { // if the TRANS tab were previously on screem if(b6 & 0x20) // if the TRANS->LIST was being displayed T6transList(); // re-display the TRANS->LIST sub-sub-tab else if(b6 & 0x40) // else if the TRANS->STATUS was being displayed T6status(); // re-display the TRANS->STATUS sub-sub-tab else T6showTrans(); // otherwise re-display the normal TRANS sub-tab } else if(a6 & 2) T6showSubTab1(); else if(a6 & 4) T6showSubTab2(); else if(a6 & 8) T6showSubTab3(); else if(a6 & 16) T6showSubTab4(); else if(a6 & 32) T6showSubTab5(); else if(a6 & 64) T6showSubTab6(); T6showHorizButs(); // display the tab's buttons } void T6clearBoxes() { // CLEAR THE INVOICE DATA BOXES int v = 86; // y-coord of top of box area XSetForeground(XD,XG,BLACK); // colour to clear all boxes for(int i = 0; i < 10; i++) { // for each field of data XDrawRectangle(XD,XW,XG,X6-3,v,53,16); // clear data box v += 20; // drop down to next field } } void T6drawBox(int c) { // draw box in YELLOW or GREEN XSetForeground(XD,XG,c); /* set the specified colour for the box vertical top corner of box to be displayed | outer box width of field 'f6' | | */ XDrawRectangle(XD,XW,XG,X6-3,ey-13,53,16); // draw the box } void T6unClick() { b6 &= ~0xFF; // dim all buttons T6showVertiButs(); // show the new states of the buttons T6showTrans(); // re-display the clean transaction record } /* BOX A FINANCIAL TRANSACTION FIELD TO EDIT OR UPON WHICH TO SEARCH. Called from T6transFieldClick(), T6transUpDn() and T6transNEW(). */ void T6transEdit() { if(f6 < 0 || f6 > 9) { // click below bottom field so kill click and exit T6unClick(); return; // dim buttons, unbox fields and exit } ex = X6; /* x-coord of start of field specified screen field number [0 to 13] | times 18 because there are 20 pixels per line | | base height of lettering for the top field | | | */ ey = f6 * 20 + Y6; /* base height of lettering for the specified field | global variable set for use elsewhere */ int i, C = YELLOW; /* loop variable + box colour for an EDIT Set up Line Editor ready for the user to EDIT a previously selected field of a transaction record or the first/next field of a NEW transaction record. */ if(b6 & 8) { /* IF EDITING AN EXISTING ENTRY: file handle for 'fin.dat' | record number of currently displayed transaction | | multiply by 32 bytes per record | | | number of the field being edited | | | | multiply by 4 bytes per field [32-bit integer] | | | | | */ fseek(FD,(r6 << 5) + (f6 << 2),SEEK_SET); /* start byte of selected field load the field content as an integer | from the transactions file 'fin.dat' | | into an 8-character array | | | with leading spaces | | | | */ char *s = showInt(getInt(FD),8,0); // convert it to character form for(i = 0; i < 8; i++) // copy it into Line Editor array le[i] = *(s+i); en = 8; // set Line Editor content length to 8 characters } else { // ELSE, IT MUST BE A NEW ENTRY OR A FIND for(i = 0; i < 8; i++) le[i] = 0; // so clear the Line Editor array en = 0; // and set its content length to zero if(b6 & 1) { // if the FIND button was clicked f6 = 1; // set to box Field 1 [the second field] C = GREEN; // colour for a FIND operation } else // otherwise we must be entering a NEW record C = BLU; // so box its filed in blue } T6drawBox(C); // draw coloured box around the field ec = 0; // set line editor's cursor position to start of field doCursor(1); // display the cursor ea = 1; // activate the line editor } /* MOUSE CLICKED ON A NON-BUTTON PART OF THE TAB. The click is therefore presumed to be an intentional click within a transaction data field. Called from 1 place in T6click(). */ void T6transFieldClick() { if(!(b6 & 0x18)) return; // provided the EDIT or NEW button is on showMsg(0); // clear the message line if(mx < 198 || mx > 249) { // if click wasn't in a data field T6unClick(); return; // dim buttons, unbox fields and exit } int i, y = 87; // y-coordinate of top field area for(i = 0; i < 10; i++) { // for each of the 10 data fields if(my > y && my < y+14) // if mouse in vertical limits of this field break; // break out of loop: i = field number y += 20; // drop down to the next field } if(i > 9) return; // click was not in one of the 10 fields f6 = i; // set the current field number T6transEdit(); // go edit selected field } void T6transUpDn() { // MOVES TO NEXT OR PREVIOUS FIELD IN TRANS SUB-TAB if(b6 & 0x20) // if the LIST button is on T6transScroll(); // scroll the transactions list else if(b6 & 0x19) { // if FIND, EDIT or NEW button on 00011001 doCursor(0); // clear the cursor do { /* if we did a mouse-wheel 'up' or hit an 'up-arrow' key | and on incrementing the field number | | it overshot the last editable field | | | */ if(mw == 1 && ++f6 > 8) f6 = 0; // loop back down to the first field else /* if we did a mouse-wheel 'down' or hit a 'down-arrow' key | and on decrementing the field number | | it undershot the first field | | | */ if(mw == 2 && --f6 < 0) f6 = 9; // loop back up to the last Field } while(f6 == 1 || f6 == 7); // skip fields 1 & 7, which is not editable T6showTrans(); // display blank form with new transaction number T6transEdit(); // set up and box the new field } mw = 0; // show that the mouse or key up/down operation is done } /* THE MOUSE WHEEL WAS MOVED WHILE WITHIN TAB 6 Called from only one place in MEH() the Mouse Event Handler. */ void T6mouseWheel() { if(a6 & 1) T6transUpDn(); } /* THE ENTER [CARRIAGE-RETURN] KEY HAS BEEN PRESSED AFTER EDITING ONE OF THE TRANSACTION RECORD FIELDS. Called from CR(). */ void T6transCR() { le[8] = 0; // terminate the line editor string for atoi() int x = atoi(le); // convert the Line Editor content to an integer if(b6 & 1) { // if we're doing a FIND r6 = x; // set record number as Line Editor content b6 &= ~0xFF; // dim all buttons T6showTrans(); // go display the retrieved transaction record showMsg(25); // show 'Transaction record found.' message } else { /* else we're doing an EDIT or NEW, so: file handle for 'fin.dat' | record number of currently displayed transaction | | multiply by 32 bytes per record | | | number of the field being edited | | | | multiply by 4 bytes per field [32-bit int] | | | | | */ fseek(FD,(r6 << 5) + (f6 << 2),SEEK_SET); // start byte of clicked field putInt(x,FD); // store the edited content as an integer fflush(FD); // make sure it is physically stored to disk T6showTrans(); // re-display the transaction record if(b6 & 0x10) { // if the NEW button is lit mw = 1; // set to do a mouse-down [or down-arrow] T6transUpDn(); // do a mouse-down } showMsg(23); // 'the edited field content has been stored.' } } /* THE NEW BUTTON ON THE TRADE->TRANS SUB-TAB HAS BEEN CLICKED. Finds a new vacant transaction record in the transactions file 'fin.dat'. Called only from one place in T6transButClick(). */ void T6transNew() { int i, j; // loop variables if(b6 & 0x10) // if the NEW button is on [bright] T6unClick(); // dim all buttons and unbox all fields else { // else the NEW button is off [dim], so b6 &= ~0xFF; // dim all the vertical TRANS buttons b6 |= 0x10; // and light the NEW button if(N6 < 255) { // if unused records still exist in 'fin.dat' fseek(FD,0,SEEK_SET); // seek byte zero of 'fin.dat' fputc(++N6,FD); // put incremented number of transaction records } else { // otherwise 'fin.dat' is full [N6 = 255], so: char s[32]; // for content of one transaction record int B = 64; // start byte of Record 2 of 'fin.dat' for(i = 2; i < 256; i++) { // for records 2 to 255 of 'fin.dat' fseek(FD,B,SEEK_SET); // seek the start byte of the record for(j = 0; j < 32; j++) // get the record's 32 bytes of content s[j] = fgetc(FD); fseek(FD,B-32,SEEK_SET); // seek the start byte of the record below for(j = 0; j < 32; j++) // and store the content in that record fputc(s[j],FD); B += 32; // then advance to start byte of next record } } fseek(FD,N6 << 5,SEEK_SET); // seek the start byte of the new record for(i = 0; i < 32; i++) // set all of its 32 bytes to zero fputc(0,FD); fflush(FD); // make sure the new state of 'fin.dat' is on disk r6 = N6; // make the highest occupied record the current record f6 = 0; // set to edit the first field of the new record T6showTrans(); // display a clear form with transaction number only T6transEdit(); // draw yellow box around selected field and edit it } T6showVertiButs(); // to show the new state of the NEW button } /* MOUSE CLICK HANDLER FOR TAB 6. Used to determine which button in the vert- ical column was clicked. Called only from T6click() below. */ int T6transButClick() { if(mx < 17 || mx > 69) return(0); // exit if outside horizontal buttons limits int i, y = 84; // height [y-coord] of top of top button for(i = 0; i < 8; i++) { // for each of the 8 vertical buttons if(my > y && my < y+21) // if mouse within vertical limits of this break; // button, break out of loop: i = button number y += 27; // drop down to the next button } if(i > 7) return(0); // click was above top or below bottom button switch(i) { // SWITCH ON BUTTON NUMBER 0 TO 7 case 0: // FIND key was clicked if(!(b6 & 0x60)) { // provided the LIST and STATUS buttons are off T6clearBoxes(); // clear any other yellow box that may exist b6 &= ~0x18; // dim the FIND and EDIT buttons ~00011000 if(b6 & 1) // the FIND button is bright, so T6unClick(); // dim ll buttons and unbox all fields else { // else the FIND button off [dim] b6 |= 1; // so switch it on [make it bright] f6 = 1; // set the current field number to Field 1 T6transEdit(); // go edit selected field } T6showVertiButs(); // to show the new state of the EDIT button } break; case 1: // NEXT key was clicked if(!(b6 & 0x60)) { // provided the LIST and STATUS buttons are off b6 &= ~0x19; // dim FIND, EDIT and NEW buttons ~00011001 if(r6 < N6) r6++; // increment record number if less than highest else r6 = 1; // otherwise wrap back down to Record 1 T6showTrans(); // re-display the transaction record } break; case 2: // PREV key was clicked if(!(b6 & 0x60)) { // provided the LIST and STATUS buttons are off b6 &= ~0x19; // dim the FIND, EDIT and NEW buttons ~00011001 if(r6 > 1) r6--; // decrement record number if greater than 1 else r6 = N6; // else wrap back up to the top of the file T6showTrans(); // re-display the transaction record } break; case 3: // EDIT key was clicked if(!(b6 & 0x60)) { // provided the LIST and STATUS buttons are off T6clearBoxes(); // clear any other yellow box that may exist b6 &= ~0x11; // dim FIND and NEW buttons ~00010001 if(b6 & 8) // if the EDIT button on T6unClick(); // dim all buttons and unbox all fields else b6 |= 8; // else EDIT button is off, so switch it on T6showVertiButs(); // to show the new state of the EDIT button } break; case 4: if(!(b6 & 0x60)) // provided the LIST and STATUS buttons are off T6transNew(); // the NEW key was clicked break; case 5: // the LIST button was clicked if(!(b6 & 0x19)) { // provided the FIND, EDIT and NEW buttons are off b6 &= ~0x59; // dim FIND, EDIT, NEW and STATUS buttons ~01011001 if(b6 & 0x20) { // if the LIST button is bright b6 &= ~0x20; // make it dim T6showTrans(); // and re-display the transaction record } else { // else the LIST button is dim, so b6 |= 0x20; // brighten the LIST button T6transList(); // display the transaction list }} break; case 6: // The STATUS button was clicked if(!(b6 & 0x19)) { // provided the FIND, EDIT and NEW buttons are off b6 &= ~0x39; // dim the FIND, EDIT, NEW & LIST buttons ~00111001 if(b6 & 0x40) { // if the STATUS button is bright b6 &= ~0x40; // make it dim T6showTrans(); // and re-display the transaction record } else { // else the STATUS button is dim, so b6 |= 0x40; // brighten the STATUS button T6status(); // display the status information }} break; case 7: // the HELP key was clicked helpPage("software/fep.html#consu"); } return(1); // return that a button was clicked } /* STORE THE SEARCH TERM IN THE APPROPRIATE INDEX RECORD'S SEARCH TERM FIELD Called 3 times from T6transSort() below. */ void T6transPutIdx(char *s, int i, int I) { fseek(FD,I,SEEK_SET); // start byte of next index record fputc(i,FD); // put main record number in index record fputc(C6,FD); // put colour [not used for content length] for(int k = 0; k < 14; k++) // put character array in the search term fputc(*(s+k),FD); // field of the index record } void T6putIdxRec(FILE *F, int B, int i, int I) { char S[15]; fseek(F,B,SEEK_SET); // seek short CLIENT NAME int K = fgetc(F), k; // get 1st byte content length [unsigned char] if(K > 14) K = 14; // limit the name length to 14 characters for(k = 0; k < K; k++) // for each character of the CLIENT NAME S[k] = fgetc(F); // get the byte from the disk record while(k < 14) { // if there is less than 14 characters in S[k] = 0; k++; // the name, fill in the remaining characters } // up to 14 with null characters T6transPutIdx(S,i,I); // store it in the index record } /* SORT THE INDEX FILE CORRESPONDING TO FIELD HEADING 'o6'. NOTE: this func- tion is not entered if o6=1 DOC. Called only by T6transSet(0). 'o6' is the column heading number 0 to 7 [including the DOC column o6=1]. But only 6 of the columns are indexed. DOC is simply the data record number in 'fin.dat'. To get the index number in the 'crop.idx' file, we have to miss out the DOC column o6=1 and make NAME [o6=2] the 2nd index as follows: FORMAT OF TRANSACTION DATA SECTION OF 'fin.dat' o6 o OS BYTE [OS=offset in bytes of 00 -- 04 0000 document number [not indexed] 01 00 00 0000 transaction date 02 01 08 0000 ref number of client in 'nad.dat' 03 02 12 0000 ref number of product in 'crop.dat' 04 03 16 0000 price 05 04 20 0000 quantity 06 05 24 0000 value 07 06 30 0000 date paid int a = getInt(FD), // transaction date b = getInt(FD), // transaction type c = getInt(FD), // ref number of client in 'nad.dat' d = getInt(FD), // ref number of product in 'crop.dat' e = getInt(FD), // price f = getInt(FD), // quantity g = getInt(FD); // date paid FORMAT OF THE 7 TRANSACTION INDEXES SECTION OF 'fin.dat' 1) DATE, 2) NAME, 3) PRODUCT, 4) PRICE, 5) QUANT, 6) VALUE, 7) PAID 0 transaction record reference [16 bytes each] 0 content length [but used in this case cor filed display colour] AAAAAAAAAAAAAA TEXTUAL CONTENT */ void T6transSort() { showMsg(20); // show the 'sorting' message int o = o6 - 1, // index number [0 to 6] x, W[] = {0,2,3,4,5,4,6}, /* number of data field required START BYTE OF THE REQUIRED DATA FIELD IN 'Record 1' of 'fin.dat' | start byte of Record 1 of the transactions data section | | [each transaction record has 32 [0x20] bytes] | | number of the data column within the LIST | | | index number of the index within 'fin.dat' to be sorted | | | | times 4 to get start byte of required field | | | | | */ B = 0x20 + (W[o] << 2), /* START BYTE OF THE CURRENT INDEX 'o' WITHIN 'fin.dat' | skip over the 256 32-byte records of the transaction data section | | plus a further 16 bytes to start of 'Record 1' of first index | | | index number of the index to be sorted | | | | times 4096 bytes per index [256 16-byte records] | | | | | */ I = 0x2010 + (o << 12), i; // loop variable: index record number within selected index for(i = 1; i <= N6; i++) { // BUILD THE UNSORTED INDEX FILE fseek(FD,B,SEEK_SET); // start byte of field in next data record switch(o) { // switch on index number 0,1,2,3,4,5,6 case 0: case 3: case 4: /* DATE, PRICE, QUANT or VALUE store in the index record of start byte 'I' | as a string of numeric characters | | the integer value in the field | | | from the current 'fin.dat' data record | | | | as a 14 character string | | | | | with leading spaces | | | | | | number of fin.dat data record | | | | | | | start byte of index record | | | | | | | | */ T6transPutIdx(showInt(getInt(FD),14,0),i,I); break; case 1: /* CLIENT NAME file handle for 'nad.dat' [contains client names] | get client number from transaction record | | file handle for transactions file 'fin.dat' | | | times 1024 bytes per record | | | | search NAME field offset | | | | | data record number | | | | | | index record start byte | | | | | | | */ T6putIdxRec(FA,(getInt(FD) << 10) + 409,i,I); break; case 2: /* PRODUCT NAME file handle for 'crop.dat' [contains CROP NAME] | get the crop type number from the crop record | | handle for transactions file 'fin.dat' | | | times 64 bytes per record | | | | number of 'fin.dat' data record | | | | | start byte of the index record | | | | | | */ T6putIdxRec(FC,getInt(FD) << 6,i,I); break; case 5: // VALUE x = getInt(FD) * getInt(FD); // value = price x quantity T6transPutIdx(showInt(x,14,0),i,I); // see case 0: break; case 6: /* PAID? If the elapsed time 'x' is negative [i.e. the payment has been made in advance]then make it into a negative 10s complement of the value. This will cause it to fall into the correct casting order for neg- ative elapsed times when they are formed into a sign + 3 digit string to put in the index record in place of the actual value. | | number of current 'fin.dat' data record | | times 32 to get its start byte | | | */ if((x = T6getDays(i << 5)) < 0) x = -(x + 999); /* 14-char numeric with sign with leading zeros elapsed time | number of 'fin.dat' data record | | | | start byte of the index record | | | | | */ T6transPutIdx(showInt(x,14,1),i,I); break; } I += 16; // advance to start byte of next index record in 'fin.dat' B += 32; // advance to start byte of next data record in 'fin.dat' } FS = FD; /* set file handle for the Hoare sort number of equivalent 16-byte records in data section of 'fin.dat' | number 0 to 6 of the index being sorted | | times 256 16-byte records per index | | | */ i = 0x200 + (o << 8); /* record zero of the index to be sorted number of the first included record of the sort range | number of the last included record in the sort range | | */ idxSort(i+1,i+N6); // sort the newly-built index file fflush(FD); // make sure everything is written to disk } /* REGISTER THAT A TRANSACTION FIELD HAS BEEN MODIFIED [EDITED OR NEW]. The 'field modified' bits are stored in Byte 34 of 'fin.dat'. A field's 'modified' bit is set every time a field's content is new or has been ed- ited, indicating that its corresponding index field needs to be re-sorted. A field's 'modified' bit is unset every time the index file for that field has just been re-sorted. Called from T6cropListClick(), T6transNameSave(), T6cropCR(). o6=7 PAID? When a bit is set, it means that o6=6 |VALUE the corresponding column's index 06=5 ||QUANTITY kilograms needs to be sorted before being 06=4 |||PRICE per kilogram displayed. If the bit is not set, 06=3 ||||PRODUCT [crop] the column's index doesn't need 06=2 |||||NAME of client resorting so can be displayed 06=1 ||||||DATE of transaction 06=0 |||||||DOCument number [not indexed] |||||||| W6 = 11111110 stored in Byte 3 of record zero of 'fin.dat' */ void T6transSet(int s) { // s=0 unset bit; s=1 set bit; 3=sort if bit set if(o6 == 0) return; // DOCument number does not have an index file int d = 1 << o6; // set bit in position corresponding to field 'o6' switch(s) { case 0: W6 &= ~d; break; // unset the 'field modified' bit case 1: W6 |= d; break; // set the 'field modified' bit case 2: T6transSort(); // sort if 'field modified' bit set T6transSet(0); // re-enter to unset the field's 'field modified' bit return; // return to re-enter this function with s=0 } fseek(FD,3,SEEK_SET); // seek byte 3 of 'fin.dat' fputc(W6,FD); // set bit for modified field and restore the byte fflush(FD); // make sure it is actually put to disk } /* HANDLES MOUSE CLICKS ON THE TRANSACTIONS LIST COLUMN HEADINGS Called from only one place in T6click(). */ void T6transListClick() { int i = -1; if(my > 54 && my < 76) { // headings buttons height limits if(mx > 85 && mx < 108) i = 0; // DOC heading clicked else if(mx > 111 && mx < 160) i = 1; // DATE heading clicked else if(mx > 162 && mx < 238) i = 2; // CLIENT NAME heading clicked else if(mx > 240 && mx < 322) i = 3; // CROP NAME heading clicked else if(mx > 324 && mx < 358) i = 4; // PRICE heading clicked else if(mx > 360 && mx < 394) i = 5; // QUANTITY heading clicked else if(mx > 396 && mx < 436) i = 6; // VALUE heading clicked else if(mx > 438 && mx < 472) i = 7; // PAID? heading clicked if(i != -1) { // provided the click was on a heading button l6 = 0; // clear index number of highlighted item b6 &= ~0xFF00; // switch off all the heading column highlighting switch(i) { case 0: if(!(b6 & 0x0100)) { b6 |= 0x0100; } break; // DATE case 1: if(!(b6 & 0x0200)) { b6 |= 0x0200; } break; // DOC number case 2: if(!(b6 & 0x0400)) { b6 |= 0x0400; } break; // CLIENT case 3: if(!(b6 & 0x0800)) { b6 |= 0x0800; } break; // PRODUCT case 4: if(!(b6 & 0x1000)) { b6 |= 0x1000; } break; // PRICE case 5: if(!(b6 & 0x2000)) { b6 |= 0x2000; } break; // QUANTITY case 6: if(!(b6 & 0x4000)) { b6 |= 0x4000; } break; // VALUE case 7: if(!(b6 & 0x8000)) { b6 |= 0x8000; } // PAID? } o6 = i; // only update o6 if a column heading has actually been clicked } } /* Else the click could have been on one of the 11 list items. So if indeed the click was on one of the list items, compute the record number 'lo', within the index file, of the highlighted item. */ else if((i = listClick()) > 0) l6 = i + g6; T6transSet(2); // sort index if field o6's 'field modified' bit is set T6transList(); // list the [sorted] crops } /* MOUSE CLICK HANDLER FOR TAB 6. Deals with clicks within the TRADE tab. Used to determine which button in the vertical column was clicked or which of the invoice data fields was clicked. Called from only one place in the Mouse Events Handler MEH().*/ void T6click() { if(tb != 6) return; // safety exit if(!(a6 & 1)) return; // exit if not in the TRANS sub-tab if(T6transButClick()) return; if(b6 & 0x20) // if the LIST button is on T6transListClick(); else // else must be in normal TRADE->TRANS sub-tab T6transFieldClick(); } /* HANDLES A 'PgUp' OR 'PgDp' KEY STROKE. Eleven lines of the CROPS list is displayed on the screen. A 'PgDn' keystroke causes the list to advance by only 10 lines, leaving what was the bottom line of the list as its new top line. This gives better user confidence and sense of continuity. */ void T6PgUpDn(int s) { // 's' = +10 for PgDn and -10 for PgUp if(a6 & 1 && b6 & 0x20) { // switch on button logic g6 += s; // increment/decrement mouse bias by 10 lines if(s < 0) { // if it was a 'Page-Up' key-stroke if(g6 < 0) g6 = 0; // if undershot beginning of list set to beginning } else { // if it was a 'Page-Down' key-stroke int y = N6 - 11; // if we've overshot the end of the list if(g6 > y) g6 = y; // set to display the final 11 lines } T6transScroll(); // re-display the diary list } } void T6transEsc() { // HANDLES WHEN THE 'Esc' KEY HAS BEEN PRESSED IN TAB 6 T6showTrans(); // re-display the transaction record showMsg(24); // 'Editing of the field has been cancelled'. } /* HANDLES CLICKS TO THE 8 BOTTOM BUTTONS OF THE TRADE TAB [TAB 6] Called from only one place in MEH(). */ void T6horizButClick(int i) { // called from only 1 place in MEH() clearTab(); a6 &= ~0xFF; // kill all buttons switch(i) { case 0: if(!(a6 & 1)) { a6 |= 1; T6showTrans(); } break; // TRANS case 1: if(!(a6 & 2)) { a6 |= 2; T6showSubTab1(); } break; // SPARE case 2: if(!(a6 & 4)) { a6 |= 4; T6showSubTab2(); } break; // SPARE case 3: if(!(a6 & 8)) { a6 |= 8; T6showSubTab3(); } break; // SPARE case 4: if(!(a6 & 0x10)) { a6 |= 0x10; T6showSubTab4(); } break; // SPARE case 5: if(!(a6 & 0x20)) { a6 |= 0x20; T6showSubTab5(); } break; // SPARE case 6: if(!(a6 & 0x40)) { a6 |= 0x40; T6showSubTab6(); } break; // TAXES case 7: if(!(a6 & 0x80)) { a6 |= 0x80; // HELP helpPage("software/fep.html#trade"); } } T6showHorizButs(); // re-display the buttons } /* FUNCTIONS PERTAINING TO TAB 5: PROJECTS ----------------------------------- /* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE CROPS SUB-TAB. Called from T5crops(), T5cropList(), T5cropButClick(). */ void T5cropShowButs() { // below are the names of the button annotations char *A[] = {"FIND","TRAWL","NEXT","PREV","NEW","KILL","LIST","HELP"}; int c, a[] = {15,12,15,15,18,15,15,15}, // horizontal offsets for word starts b[] = { 4, 5, 4, 4, 3, 4, 4, 4}, // number of letters in each word h = 99; // y coordinate of button block for(int i = 0; i < 8; i++) { // for each of the 8 buttons: XSetForeground(XD,XG,DARK); // shade for buttons' background XFillRectangle(XD,XW,XG,17,h-15,53,21); if(b5 & 1 << i) // If this button is currently 'on' c = WHITE; // make its lettering bright WHITE else // otherwise: if(i == 6 && V5) // if it is the LIST button and the 'crop alert' c = RED; // flag is set, make its lettering bright RED else c = GREY; // otherwise make its lettering GREY. XSetForeground(XD,XG,c); // set the lettering colour XDrawString(XD,XW,XG,17+a[i],h,A[i],b[i]); h += 27; // move across to the next button } } /* CLEAR THE CROP DATA BOXES. Called by T5crops(), T5cropFind(), T5cropTrawl(), T5cropNew(), T5cropFieldClick(). */ void T5cropBoxesClear() { int v = 66; // y-coord of top of box area XSetForeground(XD,XG,DARK); // colour to clear all boxes for(int i = 0; i < 10; i++) // for each field of data XDrawRectangle(XD,XW,XG,X6-3,v+=20,53,16); // clear data box XDrawRectangle(XD,XW,XG,X5-3,86,191,16); // clear product name box } /* CLEAR THE INSIDES OF ALL CROP DATA BOXES. Called from one place each in T5crops(), T5cropFind, T5cropTrawl() and T5cropNew(). */ void T5cropFieldsClear() { int v = 67; XSetForeground(XD,XG,DARK); // colour for the display field background for(int i = 0; i < 10; i++) // for each field of data XFillRectangle(XD,XW,XG,X6-1,v+=20,51,15); // clear data box XFillRectangle(XD,XW,XG,X5-2,87,190,15); // clear crop name box } /* CLEAR INSIDE OF A GIVEN CROP DATA BOX 'f5'. Called from one place each in T5cropCR() and T5cropEsc(). */ void T5cropFieldClear() { int v = 87; // y-coord of top of box area XSetForeground(XD,XG,DARK); // colour to clear all boxes if(f5 > 0 || b5 & 1) XFillRectangle(XD,XW,XG,X6-1,v += f5 * 20,51,15); // clear data box else XFillRectangle(XD,XW,XG,X5-2,87,190,15); // clear crop name box } /* SHOW THE CROP EDIT BOX. Called from 1 place each in T5cropFind(), T5cropTrawl(), T5cropNew(), T5cropFieldClick(), T5cropCR(), T5cropEsc(). */ void T5cropShowBox(int b, int c) { int v = 86; // y-coord of top of box area XSetForeground(XD,XG,c); // colour to clear all boxes if(b == 0 && !(b5 & 1)) // if it's the product name field XDrawRectangle(XD,XW,XG,X5-3,86,191,16); // show crop name box else XDrawRectangle(XD,XW,XG,X6-3,v += 20 * b,53,16); // show data box } /* DRAW HORIZONTAL ROW OF CONTROL BUTTONS ACROSS BOTTOM OF PROJS TAB Called from 1 place each in T5crops(), T5cropList(), T5horizButClick() and T5show(). */ void T5showButs() { if(tb != 5) return; // bail out if PRODS tab not currently visible showButs(); // show 8 blank buttons across the bottom of the tab area const char // annotate the horizontal row of control buttons *A[] = {"CROPS","ANIMALS","FABRI","R&D","CRAFT","ARTS","SPARE","HELP"}; const int a[] = {12, 6,12,18,12,15,12,15}, // horizontal offsets for word starts b[] = { 5, 7, 5, 3, 5, 4, 5, 4}; // number of letters in each word int h = 17, c; // x coordinate of button block for(int i = 0; i < 8; i++) { // for each of the 8 buttons: if(a5 & 1 << i) // if this button is 'on' c = WHITE; // make it lettering bright WHITE else // otherwise if(i == 0 && V5) // if it is the CROPS button and the 'crops alert' c = RED; // flag is set, make its letting RED else c = GREY; // otherwise make its lettering GREY XSetForeground(XD,XG,c); // colour for the lettering XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]); h += 59; // move across to next button } } /* GET THE NUMBER OF DAYS THAT HAS ELAPSED SINCE PLANTING OR THE NUMBER OF DAYS FROM PLANTING TO HARVEST. The file pointer [handle] for crop.dat must have been pre-set to point to the crop's DATE PLANTED field. Called from 1 place each in T5crops() & T5cropList() and from two places in T5cropSort(). */ void T5getDays(int B) { // B=start byte of main crop record fseek(FC,B+44,SEEK_SET); c5 = getInt(FC); // Bytes 44 to 47 PLANTED date d5 = getInt(FC); // Bytes 48 to 51 number of days needed to grow the crop e5 = getInt(FC); // Bytes 52 to 55 date HARVESTED j5 = 0; // set number of days before harvest to zero C5 = WHITE; // set display colour to white if(c5 == 0) { // if no crop has been planted yet [plant date is zero] e5 = 0; // set the harvest date to zero return; // exit with the elapsed time 'j5' as zero } /* Otherwise, a planting date has been specified so the crop is either still growing in the field or it has already been harvested. If the 'DATE HARVESTED' as specified in the crop record is zero, it means that the crop is still growing: it has not yet been harvested. */ if(e5 == 0) { // if the specified harvest date is zero e5 = ratSysDate(); // set 'DATE HARVESTED' to the current system date j5 = getET(c5,e5); // compute the elapsed time from planting to now if(j5 < d5) // if elapsed time still less than required growing time C5 = AMBER; // show anticipated harvest date in AMBER else { // else we're late with harvesting this crop C5 = RED; // so show the anticipated harvest date in RED V5 = 1; // and set the 'crop alert' flag } e5 = endDate(c5,d5); // compute the anticipated harvest date } /* Otherwise, the crop has already been harvested, so return the time between the planting date and the date it was harvested. */ else j5 = getET(c5,e5); } /* Product data is stored in 'crop.dat', which has 256 records of 64 bytes. The file is opened for random read/write at program start and closed at program termination. Called from 2 places in T5trawl(), and from one place each in T5cropNext(), T5cropPrev(), T5cropButClick(), T5cropFieldClick(), T5horizButClick(), T5cropCR(), T5cropEsc(), T5show(). 0 NAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Crop name [length+content] 32 bytes max 1 IIII [4-byte integer] Number of units in stock 2 IIII Number of units growing 3 IIII Location [field] 4 IIII Date when production or growing started 5 IIII Number of days production or growing time 6 IIII Date harvested 9 IIII exchange value IIII unallocated */ void T5crops() { char *M[] = { // f5 " ", // 0 after-notes "UNITS [KILOGRAMS] ", // 1 "UNITS [ANTICIPATED KILOGRAMS] ", // 2 "FIELD OR INDOOR PLANTING FACILITY ", // 3 "ZERO MEANS NOT YET PLANTED/FLOWERED ", // 4 "DAYS [ANTICIPATED GROWING TIME] ", // 5 "DAYS [TOTAL TIME TAKEN IF HARVESTED]", // 6 "DAYS TO GO TO ANTICIPATED HARVEST ", // 7 "AMBER MEANS ANTICIPATED HARVEST DATE", // 8 "VALS/KG PRICE FOR INTERNAL TRADING "}, // 9 *N[] = { // field names "CROP NAME ","QUANTITY IN STOCK", // f5=0,1 "QUANTITY GROWING ","LOCATION [FIELD] ", // 2,3 "PLANTED/FLOWERED ","GROWING TIME ", // 4,5 "TIME ELAPSED ","TIME REMAINING ", // 6,7 "DATE HARVESTED ","EXCHANGE VALUE " // 8,9 }, S[32]; // to hold the product name int I = 10, i, // loop control variables l = 20, // inter-line drop [leading] Y = Y6 - l, // set height base to first line y = Y; // set current line to first line clearTab(); // clear the whole tab area to BLACK showMsg(0); // display a blank message field XSetForeground(XD,XG,YELLOW); // set colour for bright lettering XDrawString(XD,XW,XG,17,70,"CROP DETAILS",12); T5cropBoxesClear(); T5cropFieldsClear(); XSetForeground(XD,XG,BLEEN); // set colour for bright lettering XDrawString(XD,XW,XG,X6,70,"CLICK INSIDE A FIELD TO EDIT IT",31); XDrawString(XD,XW,XG,87,309, "PRESS 'Enter' TO SAVE NEW CONTENT OR 'Esc' TO REVERT TO ORIGINAL",65); for(int i = 0; i < I; i++) // display the product FIELD NAMES XDrawString(XD,XW,XG,87,y+=l,N[i],17); y = Y; // reset current line to first line for(int i = 0; i < I; i++) // display the AFTER NOTES XDrawString(XD,XW,XG,X5,y+=l,M[i],36); XSetForeground(XD,XG,WHITE); // set colour for bright lettering fseek(FC,q5 = r5 << 6,SEEK_SET); // seek first byte of record 'r5' XDrawString(XD,XW,XG,X6,Y6,showInt(r5,8,0),8); // display CROP NUMBER y = Y6; // set base line to 'line' before 1st line I = fgetc(FC); // get the first byte of current record if(I < 0) I = 0; // invalid line length if(I > 31) I = 31; // upper safety limit for(int i = 0; i < I; i++) // for each character of the product name S[i] = fgetc(FC); // get it from the disk record XDrawString(XD,XW,XG,X5,y,S,I); // display PRODUCT NAME /* The crop name field [above] is a variable length field, consequently, we need to seek the start byte of the crop's numeric data which starts at byte 32, which is half way through the 64-byte crop data record. */ fseek(FC,q5 + 32,SEEK_SET); // seek first byte of numeric data XDrawString(XD,XW,XG,X6,y+=l,showInt(getInt(FC),8,0),8); // in stock XDrawString(XD,XW,XG,X6,y+=l,showInt(getInt(FC),8,0),8); // growing XDrawString(XD,XW,XG,X6,y+=l,showInt(getInt(FC),8,0),8); // location T5getDays(q5); // get anticipated harvest date & time remaining to harvest XDrawString(XD,XW,XG,X6,y+=l,showInt(c5,8,0),8); // plant date XDrawString(XD,XW,XG,X6,y+=l,showInt(d5,8,0),8); // days required XDrawString(XD,XW,XG,X6,y+=l,showInt(j5,8,0),8); // days gone XDrawString(XD,XW,XG,X6,y+=l,showInt(d5-j5,8,0),8); // days remaining XSetForeground(XD,XG,C5); // C5 can be can be WHITE, AMBER or RED XDrawString(XD,XW,XG,X6,y+=l,showInt(e5,8,0),8); // harvest date XSetForeground(XD,XG,WHITE); // set colour for bright lettering XDrawString(XD,XW,XG,X6,y+=l,showInt(getInt(FC),8,0),8); // price T5cropShowButs(); // display the vertical column of control buttons T5showButs(); // display the bottom row of buttons } void T5text() { char *S[] = { "This sub-tab is not implemented in this demonstration ", "version of the program. However, it's functionality is,", "for the most part, essentially the same as that of the ", "CROPS sub-tab. The names of the fields and some of the ", "interactions between them are, of course, different to ", "suit the context of this sub-tab. Implementation of it ", "is thus fairly straight-forward should it be required ", "for any particular site. " }; int y = 110; XSetForeground(XD,XG,BLEEN); for(int i = 0; i < 8; i++) XDrawString(XD,XW,XG,80,y+=19,S[i],55); } /* DISPLAY THE TITLES OF THE PROJECTS TABS. Note that all the following sub-tabs are not yet implemented and will only be implemented as and when required. Called from only 1 place each in showTab(). */ void T5anims() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"ANIMAL REARING",14); T5text(); } void T5fabri() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"FABRICATED GOODS",16); T5text(); } void T5RandD() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"RESEARCH & DEVELOPMENT PROJECTS",31); T5text(); } void T5craft() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"CRAFT PRODUCTS",14); T5text(); } void T5arts() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"WORKS OF ART",12); T5text(); } void T5spare() { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"SPARE SUB-TAB",13); T5text(); } /* FIND A NEW [SPARE, UNOCCUPIED] CROP RECORD. The first 256 bits in record zero of 'crop.dat' are used to indicate whether or not a given crops record is occupied. For instance if the tenth bit is set, it means that record 10 of 'crop.dat' is occupied with a current valid crop entry. If the tenth bit is not set, it means that record No. 10 of 'crop.dat' is unoccupied [spare] and thus may be used for a new crop's data. This func- tion finds the number of the first available spare record in which to put a new crop's details. Called only once from T5cropNew(). */ int T5unocc() { fseek(FC,0,SEEK_SET); // seek the start byte of 'crop.dat' int f = 0, // 'spare record found' flag B, // content of byte number 'i' of record zero of 'crop.dat' b, // byte for the moving test bit i, // number of the current byte within the 32-byte block j; // bit number 0 to 7 counting from left to right for(i = 0; i < 32; i++) { /* for each of the 32 bytes [of 8 bits each] get the content of byte number 'i' from record '0' of 'crop.dat' | file handle for 'crop.dat' | | if the byte is full of '1' bits | | | skip to the next byte | | | | */ if((B = fgetc(FC)) == 255) continue; b = 128; // set the moving test bit to the most significant position for(j = 0; j < 8; j++) { // for each of the 8 bits of the current byte if(B & b) b >>= 1; // if the bit is set, shift to next lower bit else { f = 1; break; } // set 'spare record found' flag; break j-loop } if(f) break; // if spare record found, break the i-loop } if(f) { // if a spare record has been found: I5 = i; /* Preserve the number of the byte, within record zero of 'crop.dat', that contains the new record's 'occupied' bit. Global variable holds the content of Byte 'i' | with the new record's 'occupied' bit set. | | Original content of Byte 'i' with the new record's bit unset. | | | | All bits are 0 except for the one representing the new record. | | | */ B5 = B | b; /* This content is copied to disk by T5cropCR() when CR is pressed after the new crop name has been entered. Global variable to hold the new record's number temporarily | until it is copied to 'r5' once CR has been pressed after | the new crop name has been entered. | | Byte number, within record zero of 'crop.dat', that contains the | | 'occupied bit pertaining to the new record. | | | | Multiplied by 8 to give the bit number of start of Byte 'i'. | | | | | | Bit number, 0 to 7 counting left to right, of the new | | | | record's 'occupied' bit within Byte 'i'. | | | | */ R5 = (i << 3) + j; return 1; // return 'new spare record found' } showMsg(5); return 0; // display 'no spare records available' message } /* REGISTERS THAT THE NEW RECORD IS NOW OCCUPIED. Sets the new record's 'occupied' bit in record zero of 'crop.dat'. increments the total number of occupied records and, if necessary, increments the highest occupied record, both in record zero of 'crop.dat'. Called from T5cropCR(). */ void T5setocc() { fseek(FC,I5,SEEK_SET); // re-seek byte 'i' containing the changed bit fputc(B5,FC); // re-write the byte to disk with new bit set fseek(FC,32,SEEK_SET); // N5 is stored in byte 32 of record zero fputc(++N5,FC); // increment & store 'number of occupied records' r5 = R5; // number of the newly created 'crop.dat' record q5 = r5 << 6; // start byte of this 64-byte 'crop.dat' record p5 = q5; // set start byte of field = start byte of record if(H5 < r5) { // If new record is beyond current highest occupied H5 = r5; // record, make it the new highest occupied record. fseek(FC,33,SEEK_SET); // H5 is stored in byte 33 of record zero, store fputc(H5,FC); // incremented number of highest occupied record } } /* TEST WHETHER A SPECIFIED CROP RECORD IS CURRENTLY OCCUPIED OR NOT Returns 1 if the crop record is currently occupied, 0 if not. Called only once from T5cropCR(). The bits that indicate which of the 256 records of the 'crop.dat' file are currently occupied are in the first 32 bytes of record zero. Example: suppose the record we want to check is record 53: so r = 53. bit 0 bit 8 bit 16 bit 24 bit 32 bit 40 bit 48 | | | | | | | 00000000 00000000 00000000 00000000 00000000 00000000 00000100 00000000 Byte 0 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 | Byte 7 bit 53 The byte number we want B = r >> 3 = 53/8 = 6. The number of places 's' we must shift the top bit of the byte [value 128] right to arrive at the record's 'occupied' bit is 53 - 48 = 5, which is r - B * 8, which is better coded as s = r - (B << 3). We must therefore shift the mask bit '128' rightwards by 's' places for it to arrive in the position of r's 'occupied' bit by doing 128 >> s. Substituting the expression for 's' we get m = 128 >> (r - (B << 3)), where 'm' is the value of the shifted mask bit. We now need to test to see if r's 'occupied' bit is set or not by AND- ing it with the mask bit 'm' and then return the value of this bit-wise AND, which is '1' if r's 'occupied' bit is set and '0' if it is not. */ int T5isocc(int r) { int B = r >> 3; // byte of record zero containing r's occupancy bit fseek(FC,B,SEEK_SET); // seek byte B /* get byte B containing record r's 'occupied' bit | AND it with the mask bit 'm', which is: | | the top bit of a byte | | | shifted left by the number of places computed as: | | | | number r of record whose occupied state we need | | | | | minus the bit number of byte B's top bit | | | | | | which is the byte number B | | | | | | | multiplied by 8 bits per byte | | | | | | | | */ return fgetc(FC) & (128 >> (r - (B << 3))); // 1=yes, 0=no } /* SET UP A TRAWL SEARCH. This searches crop name field of all the crop rec- ords for the occurrence of the entered search term. Before comparing, it converts both the search term and the retrieved field content to upper case letters only. Called only from 1 place in T5cropNext() & T5cropCR().*/ void T5trawl() { int B = r5 << 6; // start byte of record 'r5' [record number times 64] b5 |= 0x100; // set the 'trawler active' bit showMsg(6); // show message "Trawling for search term." SUC(en,st); // convert search term to upper case only /* The following loop is merely to make sure that the trawl does not loop more than once round the file 'crop.dat'. The index 'i' plays no part in- side the loop. The active stepping variable is 'r5' the number of the rec- ord currently being tested for containing the trawl search term. */ for(int i = 0; i < H5; i++) { if(T5isocc(r5)) { // provided record 'r5' is occupied int K, // length of the content of the current field k; // individual character retrieved from disk record char S[32], // to hold the field content read from the disk record *s; /* return pointer from the string comparator strstr() file handle for 'crop.dat' | start byte of current record 'r5' | | */ fseek(FC,B,SEEK_SET); /* seek start byte of current record 'r5' if the length of this field's content | as retrieved from disk | | is more than the permitted maximum of 31 bytes | | | truncate it to the permitted 31 bytes | | | | */ if((K = fgetc(FC)) > 31) K = 31; if(K < 0) K = 0; // zero it if negative // copy crop name from disk into array S[] and terminate it with a null for(k = 0; k < K; k++) S[k] = fgetc(FC); S[K] = 0; SUC(K,S); /* convert the crop name in S[] to upper case only points to character within field at which found search term begins | content of the current crop name field | | contains the entered search term | | | */ if((s = strstr(S,st)) != NULL) { // if the search term has been found T5crops(); // display crop data record with the found search term XSetForeground(XD,XG,GREEN); /* highlight for trawl search term points to start of search term in S[] x-coord of start | points to start of crop name array S[] of crop name field | | times 6 pixels per character | | | | */ XDrawString(XD,XW,XG,X5 + (s - S) * 6, Y6, s, en); /* | | | | number of chars along crop name field | | number of characters on screen to where search term starts | | in the search term | | lettering base line of crop name field | pointer to the 1st char of the search term */ showMsg(7); // display the "search term found" message in green return; // can return immediately without breaking the loops } } // end of 'provided record 'r5' is occupied' /* If, on incrementing record number r5, it has overshot the last occu- pied record H5, reset it to record 1 and reset B to record 1's start byte 64, otherwise, increment B to the start byte of the next record. */ if(++r5 > H5) { r5 = 1; B = 64; } else B += 64; } // end of the for() loop showMsg(8); // show message that the search term could not be found b5 &= ~0x100; // kill the trawl mode r5 = 1; // set back to first record T5crops(); // and display it } /* THE 'FIND' BUTTON WAS CLICKED. So clear the crop data fields and box the crop number field in green ready for entering the required crop number that you want to find. Called once in T5cropButClick(). */ void T5cropFind() { b5 |= 1; // set the crop FIND bit b5 &= ~0x100; // kill trawl mode showMsg(0); // kill the trawl message T5cropBoxesClear(); // clear any possible yellow boxes T5cropFieldsClear(); // clear the new crop display record ready for in-fill f5 = 0; // set to first field of new record [record zero] for(int i = 0; i < 48; i++) le[i] = 0; // clear the line editor array ey = 99; // line editor's vertical letter base y-coordinate ex = 200; // line editor's field start position x-coordinate ec = 0; // cursor's character position within field en = 0; // number of characters currently in the field T5cropShowBox(f5,GREEN); // display the clicked box in yellow doCursor(1); // show the cursor in green at beginning of field ea = 1; // activate [switch on] the line editor } /* SET UP A TRAWL SEARCH FOR AN ENTERED SEARCH TERM Called from only one place in T5cropButClick(). */ void T5cropTrawl() { b5 |= 2; // set the crop TRAWL bit r5 = 1; // set to first record in the file b5 |= 0x100; // set the 'trawler active' bit showMsg(0); // kill any lingering message T5cropBoxesClear(); // clear any possible yellow boxes T5cropFieldsClear(); // clear the new crop display record ready for in-fill for(int i = 0; i < 48; i++) le[i] = 0; // clear the line editor array ey = 99; // line editor's vertical letter base y-coordinate ex = 256; // line editor's field start position x-coordinate ec = 0; // cursor's character position within field en = 0; // number of characters currently in the field f5 = 0; // set to crop name entry field T5cropShowBox(f5,GREEN); // display the clicked box in green doCursor(1); // show the cursor in green at beginning of field ea = 1; // activate [switch on] the line editor } /* DISPLAY THE SCROLLABLE LIST OF CROP DATA LINES. Called from T5cropList(), T5index(), MWup() and MWdown(). */ void T5cropScroll() { XSetForeground(XD,XG,BLACK); // clear the scrolling list area XFillRectangle(XD,XW,XG,87,84,397,210); /* o5 is the column heading number 0 to 5 [including the REF column o5=4] But only 4 of the columns are indexed. REF is simply the record number within 'crop.dat'. To get the index number within the 'crop.idx' file, we have to miss out the REF column o5=4 and make CROP NAME [o5=5] the 4th index as follows: PLANTED HARVEST DAYS LOC REF CROP NAME o5 = 0 1 2 3 4 5 o = 0 1 2 3 4 */ int o = o5; // set index number to column number if(o == 5) // if column number is 5 [CROP NAME] o = 4; // CROP NAME is index 4 [REF, column 4, has no index] S5 = o * 4096; // start byte of current index 'o' within 'crop.idx' g5 = Scroller(g5,N5); // update mouse wheel position & display scroll bar int w = l5 - g5, // line number of currently selected person v = 100, i; // print base of first data line, loop variable // LIST THE CURRENT SCREEN-FULL OF CROPS for(i = 1; i < 12; i++) { // for each of the 12 lines in the list int C, x, // colours for the data line & harvest date r = i + g5; // record number of current 1st line in list if(o5 != 4) { /* if not listing in REFerence number order SEEK THE START BYTE OF RECORD 'r' WITHIN THE INDEX FILE 'crops.idx' index file handle for 'crop.dat' | skip over the crop data records | | start byte number for current index within crops.idx | | | record number within current index | | | | multiplied by 16 bytes per record | | | | | */ fseek(FC,0x4000 + S5 + (r << 4),SEEK_SET); r = fgetc(FC); /* 'crop.dat' record number from index file GET RECORD NUMBER IN 'nad.dat' OF THE CURRENTLY HIGHLIGHTED LINE number of the first currently-displayed line | list line number of first display line | | line number within complete total list in file | | | current 'nad.dat' record number of highlighted line | | | | 'nad.dat' record number from index file | | | | | */ if(i + g5 == l5) r5 = r; } else // else we are listing in plain REF number order, so r5 = l5; // the 'crop.dat' record number is simply the line number if(r > H5) break; // break if r as beyond last occupied record if(!(T5isocc(r))) continue; // skip if this record is unoccupied int B = r << 6; // start byte of the current record C = BLEEN; // colour for normal data line if(i == w) C = WHITE; // colour for the highlighted data line XSetForeground(XD,XG,C); // colour for this data line T5getDays(B); // get anticipated harvest date & time remaining to harvest /* If the PLANTED date is zero, show it as 8 dashes; otherwise display it as an 8-digit integer. */ char *s; if(c5 > 0) s = showInt(c5,8,0); else s = "--------"; XDrawString(XD,XW,XG,87,v,s,8); /* If the HARVEST date is zero, show it in normal colour as 8 dashes; else display it in colour 'C5' as an 8-digit integer. 'C5' is WHITE if the crop has already been harvested and AMBER or RED if not. */ if(e5 > 0) { s = showInt(e5,8,0); x = C5; } else { s = "--------"; x = C; } XSetForeground(XD,XG,x); XDrawString(XD,XW,XG,141,v,s,8); /* If the crop has not yet been harvested, display, in AMBER, the days left before the anticipated harvest date; otherwise display it as 3 dashes in normal colour. */ if(C5 == AMBER) s = showInt(d5-j5,3,0); else s = "---"; XSetForeground(XD,XG,C); XDrawString(XD,XW,XG,201,v,s,3); /* If the specified location of the crop [field or planting area] is non-zero, display it as an up to 3-digit number; otherwise, show it as 3 dashes, signifying that the location has not been specified. */ fseek(FC,B+40,SEEK_SET); // start byte of LOCation if((x = getInt(FC)) > 0) s = showInt(x,3,0); else s = "---"; XDrawString(XD,XW,XG,234,v,s,3); XDrawString(XD,XW,XG,264,v,showInt(r,3,0),3); // display the REF number // FINALLY, GET AND DISPLAY THE CROP NAME char S[32]; // to contain the crop name fseek(FC,B,SEEK_SET); // start byte of crop name int l = fgetc(FC); // get the first byte of current record if(l < 0) l = 0; // invalid line length if(l > 31) l = 31; // upper safety limit for(int i = 0; i < l; i++) // for each character of the product name S[i] = fgetc(FC); // get it from the disk record XDrawString(XD,XW,XG,294,v,S,l); // display crop NAME v += 19; // drop down to the next line in the list } } /* DISPLAY THE SCROLLABLE LIST OF ALL CROPS. Called from 1 place each in: T5planted(), T5harvest(), T5days(), T5loc(), T5ref(), T5name(), T5cropButClick() and T5show(). */ void T5cropList() { clearTab(); // clear the whole tab showMsg(0); // kill the trawl message int v = 70; // vertical coord of the lettering base of the title line // DISPLAY THE COLUMN HEADING BUTTONS XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,v,"CROPS:",6); XSetForeground(XD,XG,DARK); // colour for the title buttons XFillRectangle(XD,XW,XG, 87,55, 47,21); // PLANTED button XFillRectangle(XD,XW,XG,141,55, 47,21); // HARVEST button XFillRectangle(XD,XW,XG,195,55, 29,21); // DAYS button XFillRectangle(XD,XW,XG,231,55, 23,21); // LOCation button XFillRectangle(XD,XW,XG,261,55, 23,21); // REF button XFillRectangle(XD,XW,XG,291,55,192,21); // NAME button // DISPLAY ALL COLUMN HEADINGS AND HIGHLIGHT THE SELECTED ONE if(!(b5 & 0xFC00)) // if none of the column headings is selected b5 |= 0x4000; // select the default column heading: 'REF' char *G[] = {"PLANTED","HARVEST","DAYS","LOC","REF","CROP NAME"}; int H[] = {90,144,198,234,264,360}, // horiz pixel start points of headings L[] = {7,7,4,3,3,9}; // number of letters in each heading for(int i = 0; i < 6; i++) { // for each of the 6 heading buttons int c = GREY; // colour for dim button [default] if(b5 & 0x400 << i) c = WHITE; // colour for highlighted button XSetForeground(XD,XG,c); // colour for the lettering XDrawString(XD,XW,XG,H[i],v,G[i],L[i]); // show heading lettering } v = 100; // base line of the 1st data row's lettering XSetForeground(XD,XG,GLEEN); // colour for the data list lettering XDrawString(XD,XW,XG,17,318, "Click a column heading to list the crops " "in the casting order of that column.",77); T5cropScroll(); // display the scrollable list of crop data lines T5cropShowButs(); // display the vertical buttons of the crop sub-tab T5showButs(); // display the horizontal buttons of the PROJS tab } /* THE CROP SUB-TAB'S NEXT BUTTON WAS CLICKED Called only from one place in T5cropButClick(). */ void T5cropNext() { // if trawl button is on, trawl for next occurrence of search term if(b5 & 2) { if(++r5 > H5) r5 = 1; T5trawl(); return; } /* Loop up to as many times as there are records from the first [record 1] to the highest occupied record [H5] inclusive. */ for(int i = 0; i < H5; i++) { /* if after incrementing the record number r5, it overshoots the number of the highest occupied record H5, then loop round back to record 1. */ if(++r5 > H5) r5 = 1; if(T5isocc(r5)) { // If record number r5 is currently occupied, T5crops(); return; // display the record and exit. } } showMsg(18); // otherwise show the 'no next record' message } /* THE CROP SUB-TAB'S PREV BUTTON WAS CLICKED Called only from one place in T5cropButClick(). */ void T5cropPrev() { /* Loop up to as many times as there are records from the first [record 1] to the highest occupied record [H5] inclusive. */ for(int i = 0; i < H5; i++) { /* If after decrementing the record number r5, it now undershoots record 1, loop round back up to the highest occupied record H5. */ if(--r5 < 1) r5 = H5; if(T5isocc(r5)) { // If record number r5 is currently occupied, T5crops(); return; // display the record and exit. } } showMsg(19); // otherwise show the 'no previous record' message } /* THE 'NEW' BUTTON WAS CLICKED. So create a new blank record ready for editing. Called once in T5cropButClick(). */ void T5cropNew() { b5 |= 0x10; // set the crop NEW button bit b5 &= ~2; // kill trawl mode showMsg(0); // kill the trawl message if(T5unocc()) { // if an unoccupied [spare] record [q5] was found T5cropBoxesClear(); // clear any possible yellow boxes T5cropFieldsClear(); // clear the new crop display fields f5 = 0; // set to first field of new record [record zero] for(int i = 0; i < 48; i++) le[i] = 0; // clear the line editor array ey = 99; // line editor's vertical letter base y-coordinate ex = 256; // line editor's field start position x-coordinate ec = 0; // cursor's character position within field en = 0; // number of characters currently in the field T5cropShowBox(f5,YELLOW); // display the clicked box in yellow doCursor(1); // show the cursor in green at beginning of field ea = 1; // activate [switch on] the line editor } } /* THE 'KILL' BUTTON WAS CLICKED. Deletes the currently displayed crop record 'r5'. Called once in T5cropButClick(). */ void T5cropKill(){ b5 |= 0x20; // set the CROP kill bit b5 &= ~2; // kill trawl mode showMsg(0); // kill the trawl message int B = r5 >> 3; // byte of record zero containing r5's 'occupied' bit fseek(FC,B,SEEK_SET); // seek byte B within the file 'FC' [crop.dat] int x = fgetc(FC); // get the byte containing record r5's 'occupied' bit fseek(FC,B,SEEK_SET); // seek byte B again because fgetc() stepped it along /* RESTORE BYTE 'B' TO DISK WITH RECORD r5's 'OCCUPIED' BIT UNSET: content of byte B, which includes r5's 'occupied' bit | AND it with the | | INVERSE of the bit pattern of the mask bit byte, which is the | | | top bit of the mask byte | | | | shifted right by the amount required to bring it to the | | | | | position within the mask byte of r5's 'occupied' bit: | | | | | | | | | | record number of the record being deleted | | | | | | number of the byte containing r5's 'occupied' bit | | | | | | | multiplied by 8 bits per byte | | | | | | | | */ fputc(x & ~(128 >> (r5 - (B << 3))),FC); fflush(FC); // make sure the data is immediately stored on the disk } /* THE VERTICAL HELP KEY OF THE CROPS SUB-TAB WAS CLICKED [this function is not currently implemented] */ void T5cropHelp(){ printf("HELP\n"); } /* STORE THE SEARCH TERM IN THE APPROPRIATE INDEX RECORD'S SEARCH TERM FIELD Called 3 times from T5cropSort() below. */ void T5cropPutIdx(int I, int i, char *s, int J) { int j; fseek(FC,I,SEEK_SET); // start byte of field in next index record fputc(i,FC); // put main record number in index record fputc(14,FC); // put field length = 14 bytes for(j = 0; j < J; j++) // put character array in the search term fputc(*(s+j),FC); // field of the index record while(j < 14) { // fill the remainder of the index record's fputc(0,FC); j++; // search field with null characters } } /* SORT THE INDEX FILE CORRESPONDING TO FIELD HEADING 'o5'. NOTE: this fun- ction is not entered if 05 = 4 REF. Called only by T5cropSet(2). */ void T5cropSort() { showMsg(20); // show the 'sorting' message int F[] = {44,44,44,40,0,0}, // field offsets within main 'crop.dat' records B = 64 + F[o5], // start byte of requ'd field in record '1' of 'crop.dat' i,j,x; // loop variables /* o5 is the column heading number 0 to 5 [including the REF column o5=4] But only 4 of the columns are indexed. REF is simply the record number within 'crop.dat'. To get the index number within the 'crop.idx' file, we have to miss out the REF column o5=4 and make CROP NAME [o5=5] the 4th index as follows: PLANTED HARVEST DAYS LOC REF CROP NAME o5 = 0 1 2 3 4 5 o = 0 1 2 3 4 */ int o = o5; if(o == 5) o = 4; /* Ref [o5=4] does not have an index START BYTE OF RECORD 1 OF CURRENT INDEX 'o' WITHIN 'crop.dat' | skip over the 256 64-byte records of crop data | | and a further 16 bytes to the start byte of Record 1 of the index | | | index number of the selected index | | | | times 256 * 16 = 4096 bytes per index | | | | | */ S5 = 0x4010 + (o << 12); int I = S5; // initialise to start byte of selected index N5 = 0; // to re-compute number of occupied records for(i = 1; i <= H5; i++) { // BUILD THE UNSORTED INDEX FILE if(!(T5isocc(i))) continue; // skip if 'crop.dat' record is unoccupied N5++; // increment the number of occupied records char *s = "::::::::"; // high-cast string switch(o) { // SWITCH ON INDEX NUMBER 0 TO 4 case 0: // DATE PLANTED fseek(FC,B+44,SEEK_SET); /* seek Byte 44 of current crop data record Get the date PLANTED from the main crop record. If it's non-zero [i.e. it exists], put it in place of the blank high-cast string as the search term in the current record of the selected index. */ if((x=getInt(FC)) > 0) s=showInt(x,8,1); T5cropPutIdx(I,i,s,8); break; case 1: // HARVEST DATE T5getDays(B); /* compute anticipated harvest date & time remaining If the elapsed time colour has been set to AMBER, it is an anticipated harvest date, that is, the crop has not yet been harvested store it, in place of the high-cast string, as the search term in current record of the selected index. */ if(C5 == AMBER) s = showInt(e5,8,0); T5cropPutIdx(I,i,s,8); break; case 2: // DAYS TO GO BEFORE ANTICIPATED HARVEST DATE T5getDays(B); /* get elapsed time since planting indicates that the given harvest date it is an ANTICIPATED one | time remaining to anticipated harvest date | | growing time required | | | elapsed time since planting | | | | length of number including sign | | | | | put in leading zeros | | | | | | */ if(C5 == AMBER) s = showInt(d5-j5,4,1); /* start byte of current index record | current index record number | | search string: a signed 3-digit number | | | string length | | | | */ T5cropPutIdx(I,i,s,4); // store the crop index record to disk break; case 3: // LOCation [WHERE CROP IS LOCATED] fseek(FC,B+40,SEEK_SET); // seek Byte 44 of current crop data record if((x=getInt(FC)) > 0) // if LOCation is specified s = showInt(x,3,1); // put it in place of the blank high-cast string T5cropPutIdx(I,i,s,3); // store search term in appropriate index record break; case 4: // CROP NAME: put field content in index record fseek(FC,B,SEEK_SET); // start byte of crop name field int J = fgetc(FC); // get length of crop name [unsigned char] if(J > 14) J = 14; // we only search on the first 14 characters char S[14]; // to hold record content for(j = 0; j < J; j++) // get crop name from crop data section S[j] = fgetc(FC); T5cropPutIdx(I,i,S,14); // store search term in approp. index record } // end of switch I += 16; // advance to start byte of next index record B += 64; // increment to next record in 'crop.dat' } fseek(FC,32,SEEK_SET); // N5 is stored in byte 32 of record zero fputc(N5,FC); // store 'number of occupied records' FS = FC; /* set file handle for the Hoare sort skip over the 1024 equivalent 16-bytes records of the crop data | index number 0,1,2,3 or 4 of the selected index | | multiplied by 256 records per index | | | */ i = 0x400 + (o << 8); /* record zero of the selected index first occupied record of the sort range | last occupied record of the sort range | | */ idxSort(i+1,i+N5); // sort the newly-built index fflush(FC); // make sure everything is written to disk } /* REGISTER THAT A CROP FIELD HAS BEEN MODIFIED [EDITED OR NEW]. The 'field modified' bits are stored in Byte 34 of 'crop.dat'. A field's 'modified' bit is set every time a field's content is new or has been edited, indic- ating that its corresponding index field needs to be re-sorted. A field's 'modified' bit is unset every time the index file for that field has just been re-sorted. Called from T5cropListClick(), T5cropNameSave(), T5cropCR() o5=5 CROP NAME When a bit is set, it means that the o5=4 |REFerence [not used] corresponding column's index needs o5=3 ||LOCATION to be sorted before being displayed o5=2 |||DAYS before harvest If the bit is not set, the column's o5=1 ||||HARVEST index doesn't need resorting so can o5=0 |||||PLANTED be displayed immediately. |||||| 00111111 Byte 34 of record zero of 'crop.dat' */ void T5cropSet(int s) { // s=0 unset bit; s=1 set bit; 3=sort if bit set if(o5 == 4) return; // REFerence number does not have an index file fseek(FC,34,SEEK_SET); // seek byte 34 of 'crop.dat' int c = fgetc(FC), // get the existing byte [bit pattern] from disk d = 1 << o5; // set bit in position corresponding to field 'o5' switch(s) { case 0: c &= ~d; break; // unset the 'field modified' bit case 1: c |= d; break; // set the 'field modified' bit case 2: T5cropSort(); // sort the current index if sort bit set T5cropSet(0); // Re-enter to unset the field's return; // 'field modified' bit. } fseek(FC,34,SEEK_SET); // seek byte 34 of 'crop.dat' fputc(c,FC); // set bit for modified field and restore the byte fflush(FC); // make sure it is actually put to disk } /* HANDLES MOUSE CLICKS ON THE CROP LIST COLUMN HEADINGS Called from only one place in T5click(). */ void T5cropListClick() { int i = -1; if(my > 54 && my < 76) { // headings buttons height limits if(mx > 86 && mx < 134) i = 0; // PLANTED heading clicked else if(mx > 140 && mx < 188) i = 1; // HARVEST heading clicked else if(mx > 194 && mx < 224) i = 2; // DAYS heading clicked else if(mx > 230 && mx < 254) i = 3; // LOC heading clicked else if(mx > 260 && mx < 284) i = 4; // REF heading clicked else if(mx > 290 && mx < 483) i = 5; // CROP NAME heading clicked if(i >= 0) { // provided the click was on a heading button l5 = 0; // clear index number of highlighted item b5 &= ~0xFC00; // switch off all the heading column highlighting switch(i) { case 0: if(!(b5 & 0x400)) { b5 |= 0x400; } break; // PLANTED case 1: if(!(b5 & 0x800)) { b5 |= 0x800; } break; // HARVEST case 2: if(!(b5 & 0x1000)) { b5 |= 0x1000; } break; // DAYS case 3: if(!(b5 & 0x2000)) { b5 |= 0x2000; } break; // LOCation case 4: if(!(b5 & 0x4000)) { b5 |= 0x4000; } break; // REFerence case 5: if(!(b5 & 0x8000)) { b5 |= 0x8000; } // CROP NAME } o5 = i; // only update o5 if a column heading has actually been clicked } } /* Else the click could have been on one of the 11 list items. So if indeed the click was on one of the list items, compute the record number 'lo', within the index file, of the highlighted item. */ else if((i = listClick()) > 0) l5 = i + g5; T5cropSet(2); // sort index if field o5's 'field modified' bit is set T5cropList(); // list the [sorted] crops } /* MOUSE CLICK HANDLER FOR TAB 5. Used to determine which button in the vert- ical column was clicked. Called only from T5click() below. */ int T5cropButClick() { if(mx < 17 || mx > 69) // exit if outside horizontal buttons limits return(0); // return that no button was clicked int i, y = 84, q = 0; for(i = 0; i < 8; i++) { // for each of the 8 vertical buttons if(my > y && my < y+21) // if mouse in vertical limits of this button break; // break out of loop: i = button number y += 27; // drop down to the next button } if(i > 7) return(0); /* click was below the bottom button Clear all buttons 256 trawl HELP button 128 that are set 'on', 512 amber| |LIST button 64 except as shown on 1024 PLANTED|| ||KILL button 32 the right: 2048 HARVEST||| |||NEW button 16 4096 DAYS|||| ||||PREV button 8 8192 LOC||||| |||||NEXT button 4 16384 REF|||||| ||||||TRAWL button 2 32768 CROP NAME||||||| |||||||FIND button 1 |||||||| |||||||| */ b5 &= 0xFF42; // 00000000 00000000 11111111 01000010 if(i == 6) { // if the LIST button was clicked: if(b5 & 0x40) { // if the LIST button is on b5 &= ~0x1FF; // switch all buttons + trawl mode bit off if(r5 < 1) r5 = 1; // if crop number < 1 set crop number = 1 T5crops(); // display the crop details } else { // else, the LIST button is dim, so b5 &= ~0x1FF; // switch all buttons + trawl mode bit off b5 |= 0x40; // switch the LIST button on T5cropList(); // show Crops List in ref number order by default } T5cropShowButs(); // show the new state of the vertical buttons return(1); // return that a button was clicked } /* If the LIST button is on but wasn't clicked, ignore a click on any other button. return(1) means the click was not on a line of the LIST display. */ if(b5 & 0x40) return(1); switch(i) { // switch on button number 0 to 7 case 0: T5cropFind(); break; // FIND key was clicked case 2: T5cropNext(); break; // NEXT key was clicked case 3: T5cropPrev(); break; // PREV key was clicked case 4: T5cropNew(); break; // NEW key was clicked case 5: T5cropKill(); break; // KILL key was clicked case 7: T5cropHelp(); break; // HELP key was clicked case 1: // TRAWL key was pressed if(b5 & 2) // if the TRAWL button is bright b5 &= ~2; // dim it else // otherwise, TRAWL button is dim T5cropTrawl(); // so initiate a TRAWL search } T5cropShowButs(); // show the new state of the vertical buttons return(1); // return that a button was clicked } /* HANDLE A CLICK MADE ON THE CROPS TAB. Highlights [selects] one ham displayed in the hams list. Called only from MEH() */ void T5cropFieldClick() { T5crops(); // show a pristine crop record display ea = 0; // disable the Line Editor int j; // loop variables T5cropBoxesClear(); // in case click is outside any field if(my < 85 || my > 282) return; // if click was outside vertical limits if(mx > 196 && mx < 251) { // inside short fields column int y = 85; // top of top data field for(f5 = 0; f5 < 10; f5++) { // for each of the remaining 6 fields if(my > y && my < y + 16) // if click was within the f5-th field break; // exit the loop y += 20; // drop down to next field } if(f5 > 9) return; // exit is the click was not on a field if(f5 > 5 && f5 < 8) { // fields 6 & 7 can't be edited so show the showMsg(16); return; // "can't edit this field" message and exit } } else if(mx > 252 && mx < 444) // click was inside product name field f5 = 0; // field zero [product name] was clicked else return; /* click was outside any field start byte of the current CROP record [start byte of crop field zero] | this crop's record number in 'crop.dat' | | 64 bytes per record | | | */ q5 = r5 << 6; /* make the start byte of the CROP field currently being edited | the same as the start byte of current crop record | | */ p5 = q5; if(f5 == 0) { // if we're editing the CROP NAME field fseek(FC,p5,SEEK_SET); // start byte of the current crop record en = fgetc(FC); // get the first byte of current record if(en < 0) en = 0; // invalid line length if(en > 31) en = 31; // upper safety limit for(j = 0; j < en; j++) // for each character of the product name le[j] = fgetc(FC); // put it into the Line Editor's array ex = 256; // line editor's field start position } else { // else, if it is ANY OTHER FIELD j = f5; // set field number same as in disk record if(f5 > 7) // if screen field is 8 or 9 j = f5 - 2; /* subtract 2 to get field number within disk record start byte of crop field currently being edited | start byte of current CROP record [start byte of crop field zero] | | length of first field - 4 bytes | | | field number within crop record | | | | 4 bytes per field for fields 1 to 5 | | | | | */ p5 = q5 + 28 + (j << 2); fseek(FC,p5,SEEK_SET); // seek the start byte of this field /* Get the integer from the disk record, convert it to an 8-character array then copy its 8 characters into the line editor's work array. */ char *s = showInt(getInt(FC),8,0); for(j = 0; j < 8; j++) le[j] = *(s + j); le[8] = 0; // make sure it has a NULL terminator character ex = X6; // horizontal start position of line editor's field en = 8; // number of characters in line editor's field } ey = 99 + f5 * 20; // line editor's vertical letter base coord ec = (mx - ex) / 6; // cursor's character position within field ea = 1; // activate line editor T5cropShowBox(f5,YELLOW); // display the clicked box doCursor(1); // show the cursor in green } /* MOUSE CLICK HANDLER FOR TAB 5. Deals with clicks within current sub-tab. Used to determine which button in the vertical column was clicked or which of the current sub-tab's data fields was clicked. Called from only one place in the Mouse Events Handler MEH().*/ void T5click() { if(a5 & 1) { // if we're in the CROPS sub-tab if(b5 & 0x40) { // if in the Crop List display if(T5cropButClick()) // If click was on one of the buttons return; // in the vertical column, then exit. T5cropListClick(); // click was on a column heading button } else { if(T5cropButClick()) // If click was on one of the buttons return; // in the vertical column, then exit. T5cropFieldClick(); // otherwise click was in a data field } } // or in an invalid part of the tab area } /* HANDLES CLICKS ON THE HORIZONTAL BOTTOM BUTTONS OF TAB 5: THE 'PROJS' TAB Called from only 1 place in MEH(). */ void T5horizButClick(int i) { clearTab(); // clear the tab area b5 &= ~0x100; // kill CROPS trawl mode a5 &= ~0xFF; // kill all PROJect tab buttons showMsg(0); // kill the trawl message switch(i) { // IF DIM, BRIGHTEN, SHOW SUB-TAB EXIT case 0: // CROPS if(!(a5 & 1)) { // if the CROPS button is dim a5 |= 1; // illuminate the CROPS button if(b5 & 0x40) // if the CROPS->LIST button is bright T5cropList(); // re-display the Crops List else // otherwise T5crops(); // re-display the crop details } break; case 1: if(!(a5 & 2)) { a5 |= 2; T5anims(); } break; // ANIMALS case 2: if(!(a5 & 4)) { a5 |= 4; T5fabri(); } break; // FABRIcation case 3: if(!(a5 & 8)) { a5 |= 8; T5RandD(); } break; // R&D case 4: if(!(a5 & 0x10)) { a5 |= 0x10; T5craft(); } break; // CRAFT case 5: if(!(a5 & 0x20)) { a5 |= 0x20; T5arts(); } break; // ARTS case 6: if(!(a5 & 0x40)) { a5 |= 0x40; T5spare(); } break; // SPARE case 7: if(!(a5 & 0x80)) { a5 |= 0x80; // HELP helpPage("software/fep.html#projs"); } } T5showButs(); // re-display the buttons } /* [RE]DISPLAY A SINGLE FIELD OF A CROP RECORD Called from one place each in T5cropCR() and T5cropEsc(). */ void T5showCropField() { int I,i; char S[32]; // loop variables + field characters array fseek(FC,p5,SEEK_SET); // go to the first byte of the edited field if(f5 == 0 && !(b5 & 1)) { // if it was the crop name that was edited: I = fgetc(FC); // get the first byte of current record if(I < 0) I = 0; // invalid line length if(I > 31) I = 31; // upper safety limit for(i = 0; i < I; i++) // for each character of the product name S[i] = fgetc(FC); // get it from the disk record XDrawString(XD,XW,XG,X5,Y6,S,I); // display PRODUCT NAME } else /* else, IF IT'S ONE OF THE OTHER FIELDS, RETRIEVE AND DISPLAY IT: | | Y6 is really the baseline for the 1st field of the TRANSactions data | display. But it's the same value for the CROP display, so we use it. | | | | current field number in crop data display | | | times 20 pixels inter-line drop | | | | */ XDrawString(XD,XW,XG,X6,Y6 + f5 * 20,showInt(getInt(FC),8,0),8); /* | | | | | | show as a numeric character string | | | | | numeric quantity pulled from field of CROP record | | | | file handle for 'crops.dat' | | | set as an 8-digit string | | with leading spaces | display all 8 characters */ } /* WRITES THE CONTENT OF THE LINE-EDITOR ARRAY TO THE CROP'S DISK RECORD, ENSURING THAT ALL LOWER CASE LETTERS ARE MADE CAPITAL. Called from 2 palces in T5cropCR(). */ void T5cropNameSave() { int c; // to hold a single character fseek(FC,p5,SEEK_SET); // seek start byte of record fputc(en,FC); // write length of the new field content SUC(en,le); // convert to upper case only for(int i = 0; i < en; i++) fputc(le[i],FC); // write each character of crop name to disk T5cropSet(1); // set this field's 'field modified' bit } /* CARRIAGE-RETURN [ENTER] PRESSED WHEN EDITING A CROP DATA FIELD Stores the edited version of the field. Removes the yellow box. Re-gets from disk and re-displays the new field content. Called from only 1 place in the Carriage-Return Dispatcher CR(). */ void T5cropCR() { fseek(FC,p5,SEEK_SET); // go to the first byte of the edited field if(f5 == 0) { // if either top numeric field or top text field // CR PRESSED AFTER ENTERING A CROP NAME WHILE IN FIND MODE if(b5 & 1) { // if the FIND button was clicked /* If crop record number typed in to the green FIND field is | less or equal that of highest occupied record | | and is itself currently occupied | | | display the crop record 'r5'. | | | | */ if((r5 = atoi(le)) <= H5 && T5isocc(r5)) T5crops(); else showMsg(17); // otherwise display the 'not found' message return; // then in either case, exit } // CR PRESSED AFTER ENTERING A CROP NAME WHILE IN TRAWL MODE else if(b5 & 2) { // else if the TRAWL button was clicked for(int i = 0; i < en; i++) { st[i] = le[i]; // put line editor field content into search term } st[en] = 0; // array and terminate it with a NULL character r5 = 1; // start at the first record T5trawl(); // trawl for the record containing the entered name return; // and exit } // CR PRESSED AFTER ENTERING THE CROP NAME FOR A NEW CROP RECORD else if(b5 & 0x10) { // else if the NEW button is bright T5setocc(); // register the prospective record as a new record T5cropNameSave(); // save the new crop name T5crops(); // re-display the new record for editing b5 &= ~0x10; // dim the NEW button T5cropShowButs(); // redisplay the vertical button states return; // and exit } // CR PRESSED AFTER SIMPLY EDITING AN EXISTING CROP NAME else // else it was the crop name field being edited T5cropNameSave(); // so save the crop name } // CR PRESSED AFTER EDITING A FIELD OTHER THAN A CROP NAME else // else, it was one of the other fields: putInt(atoi(le),FC); // so store field to disk as an integer T5cropSet(1); // set the 'field modified' bit T5cropShowBox(f5,BLACK); // remove the yellow box T5cropFieldClear(); // clear original content from the field f5 XSetForeground(XD,XG,GREEN); // colour for field lettering T5showCropField(); } /* THE ESCAPE KEY WAS PRESSED WHILE EDITING A CROP DATA FIELD Called only from one place in Esc(). */ void T5cropEsc() { if(b5 & 0x40 || b5 & 0x100) // if the TRAWL or LIST mode T5crops(); // Redisplay the crop data else { // otherwise T5cropShowBox(f5,BLACK); // remove the yellow box T5cropFieldClear(); // clear original content from the field f5 XSetForeground(XD,XG,WHITE); // colour for field lettering T5showCropField(); // display original content of escaped field } } /* HANDLES A 'PgUp' OR 'PgDp' KEY STROKE. Eleven lines of the CROPS list is displayed on the screen. A 'PgDn' keystroke causes the list to advance by only 10 lines, leaving what was the bottom line of the list as its new top line. This gives better user confidence and sense of continuity. */ void T5PgUpDn(int s) { // 's' has a value of +10 for PgDn and -10 for PgUp if(tb != 5) return; // exit if not in Tab 5 int x = -11; // start position of current page [screen-full] switch(a5) { // switch on button logic case 1: // if we're in the LIST sub-tab g5 += s; // increment/decrement mouse bias by 10 lines if(s < 0) { // if it was a 'Page-Up' key-stroke if(g5 < 0) // if we've overshot the beginning of diary list g5 = 0; // display from the beginning of the diary list } else { // if it was a 'Page-Down' key-stroke x += N5; // start of the final 11 lines of diary list if(g5 > x) // if we've overshot the end of the diary list g5 = x; // set to display the final 11 lines } T5cropScroll(); // re-display the diary list } } /* [RE] DISPLAYS THE APPROPRIATE SUB-TAB CONTENT IN TAB 5 [PROJects] BOTH ON INITIAL EXPOSURE AND RE-EXPOSURE ON RETURN FROM ANOTHER DESKTOP OF FROM A MINIMISED STATE. Called from one place in showTab(). */ void T5show() { if(tb != 5) return; // bail out if PROJS tab not currently visible if(a5 & 1) { // if the CROPS sub tab is selected if(b5 & 0x40) // if the CROPS LIST button is on T5cropList(); // display the Crops List else T5crops(); // display the crops data screen } T5showButs(); // display the PROJ tab's bottom buttons } // FUNCTIONS PERTAINING TO TAB 4: EQUPT [EQUIPMENT] -------------------------- /* DRAW HORIZONTAL ROW OF CONTROL BUTTONS ACROSS BOTTOM OF EQUPT TAB. Called from 1 place in T4show() & T4horizButClick(). */ void T4showHorizButs() { if(tb != 4) return; // bail out if LINK tab not currently visible showButs(); // show 8 blank buttons across the bottom of the tab area const char // annotate the horizontal row of control buttons *A[] = {"UTD","FARM","NAV","SPARE","SPARE","SPARE","SPARE","HELP"}; const int a[] = {18,15,18,12,12,12,12,15}, // horizontal offsets for word starts b[] = { 3, 4, 3, 5, 5, 5, 5, 4}; // number of letters in each word int h = 17; // x coordinate of button block for(int i = 0; i < 8; i++) { // for each of the 8 buttons: if(a4 & 1 << i) // if this button should be bright XSetForeground(XD,XG,WHITE); // set colour for bright lettering else XSetForeground(XD,XG,GREY); // colour for dull lettering XDrawString(XD,XW,XG,h+a[i],377,A[i],b[i]); h += 59; // move across to the next button } } /* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE RAIN SUB-TAB. Called from T4showTab() and T4spare(). */ void T4showVertiButs(int i) { char // annotate the vertical row of control buttons *A[7][8] = { {"ELECT","HOB","OVEN","FRIDGE","WASHER","SEWING","WATER","WASTE"}, {"PLOW","DISK","HARROW","PLANT","REAP","PICK","WATER","FEED"}, {"MAPPING","PROGRAM","AREA","PROCESS","START","PAUSE","ABORT","HELP"}, {"","","","","","","",""}, {"","","","","","","",""}, {"","","","","","","",""}, {"","","","","","","",""} }; int a[7][8] = { // horiz offsets for word starts {12,18,15, 9, 9, 9,12,12}, // UTD tab {15,15, 9,12,15,15,12,15}, // FARM tab { 6, 6,15, 6,12,12,12,15}, // NAV tab {0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0} }, b[7][8] = { // number of letters in each word {5,3,4,6,6,6,5,5}, // UTD tab {4,4,6,5,4,4,5,4}, // FARM tab {7,7,4,7,5,5,5,4}, // NAV tab {0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0} }, G[] = {b4,c4,d4,e4,f4,g4,h4}, g = G[i], // current sub-tab number [bottom button number 0 to 7] h = 99; // y coordinate of button block for(int j = 0; j < 8; j++) { // for each of the 8 buttons: XSetForeground(XD,XG,DARK); // shade for buttons' background XFillRectangle(XD,XW,XG,17,h-15,53,21); if(g & 1 << j) // if this button is bright [on] XSetForeground(XD,XG,WHITE); // set colour for bright lettering else // otherwise XSetForeground(XD,XG,GREY); // set colour for dull lettering XDrawString(XD,XW,XG,17+a[i][j],h,A[i][j],b[i][j]); h += 27; // move across to the next button } } /* DISPLAY THE CONTENT OF SUB-TAB 'i': Called from 1 place in T4show() and T4horizButClick(). */ void T4showTab(int i) { char *S[3][8] = { { "Electrical power and lighting circuits.", "Electrical induction twin hob for cooking.", "Thermal, forced-air and microwave ovens.", "Fidge and chest freezer with forced-air cooled compressors.", "Horizontal drum washing machine and tumble dryer.", "Sewing and cutting machines for clothing.", "Water supply circuits for kitchen, bathroom and growing units.", "Waste water toilet-to-tap recycling system." }, { "Maintenance instructions & diary + mount command for plough.", "Maintenance instructions & diary + mount command for disk harrow.", "Maintenance instructions & diary + mount command for spike harrow.", "Maintenance instructions & diary + mount command for seed planter.", "Maintenance instructions & diary + mount command for cereal reaper.", "Maintenance instructions & diary + mount command for fruit picker.", "Maintenance instructions & diary + mount command for irrigator.", "Maintenance instructions & diary + mount command for plant feeder." }, { "Set up or alter the mapping of crop areas.", "Program the small-area hyperbolic radio navigator.", "Select which area is to be processed.", "Select the type of process to be effected upon the selected area.", "Start and monitor the process.", "Pause the process to rectify a problem before re-starting.", "Abort the process and have the machine return to base.", "Access the web-based discourse on this sub-tab." } }; int s[3][8] = { {39,42,40,59,32,41,62,43}, {60,65,66,66,67,66,63,66}, {42,50,37,65,30,58,54,47} }, v = 98; // y-coord of first annotation line clearTab(); // clear the tab area XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70, "ACTIVE DOMESTIC DEVICES OF THE UNIVERSAL TERRESTRIAL DWELLING [UDT]",67); XSetForeground(XD,XG,BLEEN); // colour for the lettering for(int j = 0; j < 8; j++) { XDrawString(XD,XW,XG,87,v,S[i][j],s[i][j]); v += 27; } XDrawString(XD,XW,XG,17,315,"Click button for usage and " //27 "configuration instructions, plus maintenance diary.",78); //50 T4showVertiButs(i); // 0=UTD sub-tab's vertical buttons showMsg(0); } /* Reset the x and y coordinate data sets for both the painter trace and the wiper trace then wipe the plotting area and re-draw the axes. Called from 1 place in T4show() and T4horizButClick(). */ void T4spare(int i) { XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70, "SPARE TAB AVAILABLE FOR FUTURE EQUIPMENT-RELATED FUNCTIONS",58); T4showVertiButs(i); showMsg(0); } /* MOUSE CLICK HANDLER FOR TAB 4. Used to determine which button in the vert- ical column was clicked. Called only from T4click() below. */ void T4vertiButClick() { if(mx < 17 || mx > 69) return; // exit if outside horizontal buttons limits int i, y = 84; // height [y-coord] of top of top button for(i = 0; i < 8; i++) { // for each of the 8 vertical buttons if(my > y && my < y+21) // if mouse within vertical limits of this break; // button, break out of loop: i = button number y += 27; // drop down to the next button } if(i > 7) return; // click was above top or below bottom button int j = 1 << i, // position bit for the clicked vertical button m = 0; // message number showMsg(0); // clear any lingering message switch(i4) { // SWITCH ON SUB-TAB BUTTON CODE case 0: // THE UTD SUB-TAB if(b4 & j) // if the first button is already bright b4 &= ~0xFF; // clear all vertical control buttons else { // else the first button must be dim b4 &= ~0xFF; // clear all vertical control buttons b4 |= j; // illuminate the first control button m = 28; // display the 'not implemented' message } break; case 1: // THE FARM SUB-TAB if(c4 & j) // if the first button is already bright c4 &= ~0xFF; // clear all vertical control buttons else { // else the first button must be dim c4 &= ~0xFF; // clear all vertical control buttons c4 |= j; // illuminate the first control button m = 28; // display the 'not implemented' message } break; case 2: // THE NAV SUB-TAB if(d4 & j) // if the first button is already bright d4 &= ~0xFF; // clear all vertical control buttons else { // else the first button must be dim d4 &= ~0xFF; // clear all vertical control buttons d4 |= j; // illuminate the first control button m = 28; // display the 'not implemented' message } break; } showMsg(m); T4showVertiButs(i4); } /* HANDLES CLICKS TO THE 8 BOTTOM BUTTONS OF THE EQUPT TAB [TAB 4] Called from only one place in MEH(). */ void T4horizButClick(int i) { // called from only 1 place in MEH() clearTab(); i4 = i; // make 'number of selected horizontal button' global a4 &= ~0xFF; // kill all buttons a4 |= 1 << i; // illuminate the clicked horizontal button if(i < 3) T4showTab(i); else if(i < 7) T4spare(i); else helpPage("software/fep.html#equpt"); T4showHorizButs(); // re-display the buttons } /* THE MOUSE WAS CLICKED WITHIN THE EQUPT TAB. Called from 2 places in MEH(). */ void T4click() { T4vertiButClick(); } /* SETS UP THE TEXT AND BUTTONS FOR THE EQUPT TAB. This is a dummy function informing the user that this Tab's functionality is not implemented in this demo version of the program. Called from 1 place in showTab(). */ void T4show() { if(i4 < 3) // if sub-tabs 0=UTD, 1=farm, 2=nav selected T4showTab(i4); // display the appropriate sub-tab else // otherwise, sub-tabs 3,4,5,6 [all spare] is seected T4spare(i4); // so display the 'spare' text T4showHorizButs(); // display the tab's buttons } // FUNCTIONS PERTAINING TO TAB 3: POWER -------------------------------------- /* DRAW THE HORIZONTAL ROW OF CONTROL BUTTONS ACROSS THE BOTTOM OF THE POWER TAB. Called from 1 place each in T3showINPUT(), T3showOUTPUT(), T3showBOTH() and T3horizButClick(). */ void T3Buts() { showButs(); // show 8 blank buttons across the bottom of the tab area const char // annotate the horizontal row of control buttons *A[] = {"SOURCE","SOLAR","WIND","APU.","BATT","CONSUM","REGEN","HELP"}; const int a[] = { 9,12,15,18,15,9,12,15}, // horizontal offsets for word starts b[] = { 6, 5, 4, 3,4, 6, 5, 4}; // number of letters in each word int h = 17; // left edge of left-most button for(int i = 0; i < 8; i++) { // for each of the 8 buttons: if(a3 & 1 << i) { // if the button is 'on' [bright] XSetForeground(XD,XG,WHITE); // display it's lettering in white } else XSetForeground(XD,XG,GREY); // default colour for the lettering XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]); h += 59; // move across to the next button } } /* SHOW PERCENTAGE GRAPHIC AND DIGITS. This is the grey box that's internally 100 pixels high to show the percentage of the power that is supplied from sustainable sources. The green infill of the graphic is 100 pixels [100%] high by 40 pixels wide. percentage 0 to 100 | x-coord of left side of graphic box | | y-coord of top of graphic box | | | */ void T3percent(int p, int h, int v) { /* if the percentage submitted is more than 100 | or less than zero | | */ if(p > 100 || p < 0) return; // bail out XSetForeground(XD,XG,BLACK); // set colour for lettering XFillRectangle(XD,XW,XG,h+2,v+2,40,100); // clear the percentage graphic XFillRectangle(XD,XW,XG,h,v+9,24,9); // and the percentage readout XSetForeground(XD,XG,GREY); // set colour for lettering XDrawRectangle(XD,XW,XG,h+1,v+1,41,101); // show the percentage box XDrawLine(XD,XW,XG,h+50,v+1,h+50,v+101); // show vertical scale line for(int i = 0; i < 101; i += 10) { // show horizontal graduation marks XDrawLine(XD,XW,XG,h+50,v+1+i,h+55,v+1+i); if(i > 0 && i < 100) // and percentage annotations XDrawString(XD,XW,XG,h+58,v+6+i,showInt(100-i,2,0),2); } XSetForeground(XD,XG,GREEN); // fill the box in bright green XFillRectangle(XD,XW,XG,h+2,v+102-p,40,p); // to the current percentage int a = 0, // positioning variables for the 3-character percentage readout b = 2; XSetForeground(XD,XG,WHITE); // show readout figures in white if(p == 100) { // if reached exactly 100% a = 3; b = 3; // set for a 4-character readout '100%' } XDrawString(XD,XW,XG,h+13-a,v+118,showInt(p,b,1),b); // show figures XDrawString(XD,XW,XG,h+26+a,v+118,"%",1); // show % sign } /* POWER CONSUMPTION SUB-TAB */ void T3cons() { char *B[] = {"12V DC EMERGENCY", "12V DC ESSENTIAL", "12V DC LIGHTING ", "12V DC COMPUTERS", "12V DC LOW POWER", "115V 400HZ POWER"}; int H = 260, // left edge of second column C[12], // current percentage charge of the battery cells S[12], // current specific gravity of battery cell acid in SGUs T[12], // temperature of battery cell in tenths of degree centigrade v = 70; // vertical coordinate of initial line of text lq = 0; // charge percentage of the battery for the Left-Hand Bus rq = 0; // charge percentage of the battery for the Right-Hand Bus XSetForeground(XD,XG,BLACK); // clear the tab area XFillRectangle(XD,XW,XG,16,40,480,350); showMsg(0); // display blank message area XSetForeground(XD,XG,YELLOW); XDrawString(XD,XW,XG, 17,v,"THE CURRENT ELECTRICITY CONSUMPTION RATES:" " FOR THE LEFT AND RIGHT-HAND BUSES",76); v += 20; XSetForeground(XD,XG,BLEEN); // clear the tab area XDrawString(XD,XW,XG,17,v,"-------FOR THE LEFT-HAND BUSES------",36); XDrawString(XD,XW,XG, H,v,"-------FOR THE RIGHT-HAND BUSES-----",36); v+= 20; XSetForeground(XD,XG,YELLOW); XDrawString(XD,XW,XG, 17,v,"----BUS NAME---- MJd %",23); XDrawString(XD,XW,XG,H,v,"----BUS NAME---- MJd %",23); int p = 17; v+=20; XSetForeground(XD,XG,GREY); for(int i = 0; i < 6; i++) { XDrawString(XD,XW,XG, 17,v,B[i],16); XDrawString(XD,XW,XG,H,v,B[i],16); v += p; } v = 130; int h = 17, c=0, d=0; FILE *F = fopen("data/cons.dat","r"); // open the consumption data file XSetForeground(XD,XG,BLEEN); // clear the tab area for(int i = 0; i < 12; i++) { // for each of the 12 battery cells int a = getInt(F), b = getInt(F); XDrawString(XD,XW,XG,h+102,v,showInt(a,3,1),3); XDrawString(XD,XW,XG,h+126,v,showInt(b,2,1),2); v += p; if(i == 5) { v = 130; h = H; } if(i > 5) d += a; else c += a; } fclose(F); // close the battery file XSetForeground(XD,XG,YELLOW); p = 16; v -= 14; XDrawString(XD,XW,XG, 17,v+=p,"TOTAL........... MJ/day x 10",32); XDrawString(XD,XW,XG,119,v,showInt(c,3,1),3); XDrawString(XD,XW,XG,262,v,"TOTAL........... MJ/day x 10",32); XDrawString(XD,XW,XG,364,v,showInt(d,3,1),3); XSetForeground(XD,XG,BLEEN); XDrawString(XD,XW,XG, 17,v+=p+12, "MJd = Tenth-megajoules per day [average" " electrical consumption for each bus].",77); XDrawString(XD,XW,XG, 17,v+=p, "% = Each bus's contribution to the total" " electrical consumption on that side.",77); XDrawString(XD,XW,XG, 17,v+=p, "The two graphics show the average consumption" " on each bus as a percentage of",76); XDrawString(XD,XW,XG, 17,v+=p, "that respective bus's sustainable [green] generating capacity.",62); XSetForeground(XD,XG,YELLOW); T3percent(c*10/z3,171,97); T3percent(d*10/z3,H+154,97); } /* THE BATTERY SUB-TAB: Two batteries of 6 cells each. One battery supplies the left-hand bus and the other battery supplies the right-hand bus. Cell acid comprises 65% water + 35% sulphuric acid. Its specific gravity changes linearly with charge. 100% 1265 specific gravity of cell acid times 1000 measured at 27C 0% 1120 ---- ---- 100% 145 range from flat to full charge in SGUs [specific gravity x 1000] 1% 1.45 SGUs difference If observed specific gravity is s, what is the % charge? Specific gravity at full charge varies with temperature from 1294 SGU at 0C to 1266 SGU at 40C So over the 40 degree range, the specific gravity changes by 28 SGU. That is 28/40 = 0.7 SGU per centigrade degree. Each battery cell is 10cm radius by 65cm tall = 20420 cc It may thus contain 20000 cc [20 litres] of acid. 80–90 Wh/L Two batteries of 6 cells each @ 20 litres per cell = 240 litres. 240 * 80 = 19200 watt-hours = 69120000 joules = 70MJ */ void T3batt() { int C[12], // current percentage charge of the battery cells S[12], // current specific gravity of battery cell acid in SGUs T[12], // temperature of battery cell in tenths of a degree centigrade v = 70; // vertical coordinate of initial line of text lq = 0; // charge percentage of the battery for the Left-Hand Bus rq = 0; // charge percentage of the battery for the Right-Hand Bus XSetForeground(XD,XG,BLACK); // clear the tab area XFillRectangle(XD,XW,XG,16,40,480,350); showMsg(0); // display blank message area XSetForeground(XD,XG,YELLOW); XDrawString(XD,XW,XG, 17,v,"CURRENT CONDITIONS OF THE BACK-UP BATTERIES" " FOR THE LEFT AND RIGHT-HAND BUSSES",78); v += 20; XSetForeground(XD,XG,BLEEN); // clear the tab area XDrawString(XD,XW,XG, 17,v,"<---BATTERY FOR LEFT-HAND BUS--->",33); XDrawString(XD,XW,XG,250,v,"<---BATTERY FOR RIGHT-HAND BUS--->",34); v+= 20; XSetForeground(XD,XG,YELLOW); XDrawString(XD,XW,XG, 17,v,"SGUs TMP CHG",14); XDrawString(XD,XW,XG,250,v,"SGUs TMP CHG",14); v = 130; int h = 17, s, t; FILE *F = fopen("data/batt.dat","r"); // open the battery data file XSetForeground(XD,XG,BLEEN); // clear the tab area for(int i = 0; i < 12; i++) { // for each of the 12 battery cells s = getInt(F), // get the specific gravity t = getInt(F); /* get the temperature current percentage charge of the k-th battery cell | datum for computation is 100% charged | | <--percent short from 100% charge--> | | specific gravity at 100% charge at 27C acid temperature | | | actual specific gravity of cell 'k' in SGUs | | | | Density compensation factor to make the dens- | | | | ity what it would be if the acid temperature | | | | were the standard 27 degrees centigrade. | | | | | | | | | <---------------> */ double q = 100 - (1265 - s - (t - 270) * 0.07) / 1.45; /* | | | | actual temperature of cell 'k' | | | standard acid temperature 27 | | change in specific gravity per unit temperature | change in specific gravity for a 1% change in charge */ int c = round(q); if(i < 6) lq += c; else rq += c; C[i] = c; // put percentage charge of cell 'k' in array S[i] = s; // put the specific gravity of cell 'k' in the array T[i] = t; // put the temperature of cell 'k' in the array XDrawString(XD,XW,XG,h ,v,showInt(s,4,1),4); XDrawString(XD,XW,XG,h+36,v,showInt(t,3,1),3); XDrawString(XD,XW,XG,h+66,v,showInt(c,3,1),3); v += 17; if(i == 5) { v = 130; h = 250; } } fclose(F); // close the battery file XSetForeground(XD,XG,BLEEN); int p = 16; v -= 7; XDrawString(XD,XW,XG, 17,v+=p, "SGU = Specific Gravity Units in which the" " specific gravity of water is 1000.",76); XDrawString(XD,XW,XG, 17,v+=p, "TMP = Temperature of the cell's acid measured" " in tenths of degrees Celcius.",75); XDrawString(XD,XW,XG, 17,v+=p, "CHG = The percentage to which each cell" " of each battery is currently charged.",77); XDrawString(XD,XW,XG, 17,v+=p, "The total energy stored in each battery when" " fully charged is 35 megajoules.",76); XSetForeground(XD,XG,YELLOW); // clear the tab area XDrawString(XD,XW,XG, 17,v+=p+7, "Energy currently available MJ: good for" " hours at average consumption.",75); int l = lq / 6, // percentage charge of the Left-Hand Bus's battery r = rq / 6; // percentage charge of the Right-Hand Bus's battery T3percent(l,140,97); T3percent(r,375,97); s = 35 * (l + r); // total energy available (both batteries) in MJ t = 24 * s / y3; // hours of supply at average consumption XSetForeground(XD,XG,WHITE); // clear the tab area XDrawString(XD,XW,XG,179,v,showInt(s,2,1),2); XDrawString(XD,XW,XG,275,v,showInt(t,2,1),2); } void T3consum() { // megajoules per month const int E[] = {1620,1440,972,810,774,749,763,770,749,803,1530,1620}, D[] = {52,51,31,27,25,25,25,25,25,26,51,52}; // MJ per day /* Construct the graph 366 days by 55MJ to plot the above figures. */ } /* DISPLAYS AUXILIARY POWER UNIT DATA */ void T3apu(int n) { // n = landshare number XSetForeground(XD,XG,BLACK); // clear the tab area XFillRectangle(XD,XW,XG,16,40,480,350); showMsg(0); // display blank message area int a = 0, // type of apu equipment b = 1, // fuel type c = 1000, // fuel tank capacity [litres] d = 770, // litres of fuel currently in tank [1000 litres = 1 cu metre] e = 30, // average consumption per day [megajoules] f = 0; // average daily energy short-fall // DISPLAY THE INFORMATION LINE TITLES const char *S[12] = { "EQUIPMENT TYPE ", // a "FUEL TYPE ", // b "ENERGY VALUE OF THIS FUEL kJ/LITRE", "CAPACITY OF THE FUEL TANK LITRES ", // 2000 litres "QUANTITY OF FUEL CURRENTLY IN TANK LITRES ", // d "TOTAL ENERGY VALUE IN TANK MJ ", "OVERALL CONVERSION EFFICIENCY % ", "AMOUNT OF ELECTRICITY RECOVERABLE MJ ", "AVERAGE DAILY ENERGY CONSUMPTION MJ ", // e 30 MJ "AVERAGE DAILY ENERGY SHORTFALL MJ ", // f 0 MJ "SURVIVAL AT FULL CONSUMPTION RATE DAYS ", "SURVIVAL AT SHORTFALL CONSUMPTION DAYS " }; int H = 17, V = 70; XSetForeground(XD,XG,YELLOW); // draw the column titles in yellow XDrawString(XD,XW,XG,H,V,"AUXILIARY POWER UNIT [APU]",26); XDrawString(XD,XW,XG,364,V,"LANDSHARE",9); XDrawString(XD,XW,XG,364,V+55,"FUEL TANKS",10); int i, v = V + 20; for(int i = 0; i < 12; i++) { XSetForeground(XD,XG,GREY); // draw line titles in grey XDrawString(XD,XW,XG,H,v,S[i],52); v += 20; } // DEFINE ENGINE TYPE AND FUEL TYPE OPTIONS const char *E[] = { // engine types "MINIATURE WATER-INJECTED GAS TURBINE","DIESEL ENGINE","GASOLINE ENGINE"}; const int El[] = {36,13,15}; // number of letters in each string const char *F[] = { // fuel types "DIESEL OIL","RAPE SEED OIL","KEROSENE","GASOLINE", "LIQUIFIED PETROLEUM GAS","ETHANOL"}; const int Fl[] = {10,13,8,8,23,7}, // number of letters in each string /* ENERGY VALUE OF FUEL [KILOJOULES PER LITRE] diesel, rape oil, kerosene, gasoline, LPG, ethanol */ P[] = { 37400, 34385, 34265, 33339, 26796, 23439 }, /* ENERGY CONVERSION EFFICIENCY OF ENGINE TYPE [PERCENT] diesel, turbine, gasoline [turbine is water-injected] */ Q[] = { 50, 54, 40 }; // DISPLAY THE ENGINE TYPE AND FUEL TYPE XSetForeground(XD,XG,BLEEN); v = V; // vertical coordinate for text item int l = El[a]; XDrawString(XD,XW,XG,H+(52-l)*6,v+=20,E[a],l); l = Fl[b]; XDrawString(XD,XW,XG,H+(52-l)*6,v+=20,F[b],l); // COMPUTE THE ENERGY VALUES int p = P[b]; // energy value of selected fuel [kJ/litre] double T = (double)d * (double)p / 1000, // total energy in tank [megajoules] x = (double)Q[a] * 72 / 10000, // overall conversion efficiency R = T * x; // electrical energy realisable [MJ] // COMPUTE SURVIVAL TIMES int g = 0; // survival time at full daily consumption rate [days] l = 0; // survival at short-fall daily consumption rate [days] if(y3 > 0) g = round(R / y3); if(f > 0) l = round(R / f); int N[] = { p, // energy value of selected fuel [kilojoules per litre] c, // fuel tank capacity [litres] d, // amount of fuel currently in the tank [litres] (int)round(T), // total thermal energy in tank [megajoules] (int)(x * 100), // thermal to electrical energy conversion efficiency [%] (int)round(R), // amount of electricity recoverable [megajoules] y3, // average daily energy consumption [megajoules] f, // average daily energy shortfall [megajoules] g, // survival at full daily consumption rate [days] l // survival at short-fall daily consumption rate [days] }, h = H + 210; for(i = 0; i < 10; i++) XDrawString(XD,XW,XG,h,v+=20,showInt(N[i],8,0),8); // DISPLAY THE TANK PERCENTAGE GRAPHIC v = 90; XSetForeground(XD,XG,GREY); // draw the Landshare Ref in grey XDrawString(XD,XW,XG,364,v,"REF of ",14); XSetForeground(XD,XG,BLEEN); // draw the column titles in grey XDrawString(XD,XW,XG,388,v,showInt(n,3,1),3); XDrawString(XD,XW,XG,430,v,showInt(n,3,1),3); v = 145; g = 16; XDrawString(XD,XW,XG,364,v, "Computations based ",19); XDrawString(XD,XW,XG,364,v+=g, "on fuel tank volume",19); XDrawString(XD,XW,XG,364,v+=g,"of 500 litres each.",19); T3percent(100,330,v+=g); // show the percentage full Tank 1 T3percent( 54,414,v); // show the percentage full Tank 2 } /* DISPLAYS A TABLE SHOWING THE POWER DERIVED FROM EACH SOURCE-TYPE FOR EACH OF THE 3 FORMS OF ENERGY [THERMAL, MECHANICAL, ELECTRICAL] AND A GRAPHIC SHOWING THE PERCENTAGE OF THE TOTAL POWER THAT'S RENEWABLE [SUSTAINABLE] */ void T3source(int n) { // n = landshare number FILE *F = fopen("data/land.dat","r+"); // open the landshare data file /* record number of this landshare | multiplied by 2048 [record size in bytes] | | plus the offset of the first power field | | | */ fseek(F,(n << 11) + 1450,SEEK_SET); // seek start byte of first power field for(int i = 0; i < 8; i++) // for each of the 8 power sources for(int j = 0; j < 3; j++) // for each form of energy required b3[i][j] = getInt(F); // get the average power fclose(F); // close the landshare file XSetForeground(XD,XG,BLACK); XFillRectangle(XD,XW,XG,16,40,480,350); showMsg(0); const char *S[8] = {"PETROLEUM","LP-GAS","BIO-ALCOHOL","SOLAR", "WIND","WOOD","HYDRO","GEOTHERMAL"}; const int L[8] = {9,6,11,5,4,4,5,10}; // word lengths for the above int i,j,c, h = 17, // horizontal coordinate for start of text item v = 70, // vertical coordinate for text item x,y,z, Y = 0, // for total power produced Z = 0, // for total sustainable power C[8] = {GRED,GRED,BROWN,GLEEN,GLEEN,GLEEN,GLEEN,GLEEN}; for(i = 0; i < 8; i++) { // for each of the 8 source types x = b3[i][0] + b3[i][1] + b3[i][2]; b3[i][3] = x; // total power for this source Y += x; // accumulate total power for all sources if(i > 2) Z += x; // accumulate total sustainable power } XSetForeground(XD,XG,YELLOW); // draw the column titles in yellow XDrawString(XD,XW,XG,h,v,"ENERGY SOURCE THERMAL MECHANICAL " "ELECTRICAL TOTAL % RENEWABLE %",75); v = 90; for(i = 0; i < 8; i++) { XSetForeground(XD,XG,C[i]); // draw energy source names in grey-green XDrawString(XD,XW,XG,h,v,S[i],L[i]); v += 17; } v = 90; // letter base height for first data row for(i = 0; i < 8; i++) { h = 42; // horizontal coord of start of data row for(j = 0; j < 4; j++) { y = b3[i][3]; // total power from this source for all 3 types of use if(j == 3) y = Y; x = b3[i][j]; // power from the i-th source used for the j-th purpose if(y > 0) z = x * 100 / y; else z = 0; // avoid division by zero if(x > 0) c = WHITE; else c = GREY; // grey out inactive sources XSetForeground(XD,XG,c); XDrawString(XD,XW,XG,h+=72,v,showInt(x,4,1),4); // display watts XDrawString(XD,XW,XG,h+36,v,showInt(z,2,1),2); // display percentage } v += 17; // drop down to next line of table } v+=3; XSetForeground(XD,XG,YELLOW); // draw the column totals in grey XDrawString(XD,XW,XG,17,v, "TOTAL POWER ................................",48); XDrawString(XD,XW,XG,h,v,showInt(Y,4,1),4); // display total watts XDrawString(XD,XW,XG,h+36,v,"W",1); XSetForeground(XD,XG,GREY); // draw the column titles in grey XDrawString(XD,XW,XG,390,v,"REF of ",14); XSetForeground(XD,XG,BLEEN); // draw the column titles in grey XDrawString(XD,XW,XG,414,v,showInt(n,3,1),3); XDrawString(XD,XW,XG,456,v,showInt(n,3,1),3); T3percent(100*Z/Y,400,87); // show the percentage renewable graphic v = 253; x = 16; XSetForeground(XD,XG,BLEEN); // grey-green XDrawString(XD,XW,XG,17,v,"The column headings give the type or types" " of energy supplied by each source.",77); XDrawString(XD,XW,XG,17,v+=x,"In each column, the 4-digit block gives the" " power [energy per unit time] sup-",77); XDrawString(XD,XW,XG,17,v+=x,"plied by each source, the last one being" " the total of the previous 3 columns.",77); XDrawString(XD,XW,XG,17,v+=x,"The 2-digit number is the percentage of" " the source's power used in that form.",77); XDrawString(XD,XW,XG,17,v+=x,"Percentages in last column are of each" " source's contribution to total power. ",77); } /* REGENERATE THE TEST VALUES [IN WATTS] FOR AVERAGE POWER. The power derived from each source is divided into the power of each of the three forms: Thermal power, Mechanical power and Electrical power. Storage allocation in Landshare record: */ void T3regen(int n) { /* n = number of the landshare being dealt with HEAT MECH ELEC Power [in watts] from each source */ int P[24] = { 00, 00, 00, // PETROLEUM [diesel, kerosene, gasoline] 20, 00, 00, // LP-GAS 00, 00, 00, // BIO-ALCOHOL 50, 00, 120, // SOLAR 00, 00, 150, // WIND 50, 00, 00, // WOOD 00, 00, 00, // HYDRO 00, 00, 00 }; // GEOTHERMAL FILE *F = fopen("data/land.dat","r+"); // open the landshare data file /* record number of this landshare | multiplied by 2048 [record size in bytes] | | plus the offset of the first power field | | | */ fseek(F,(n << 11) + 1450,SEEK_SET); // seek start byte of 1st power field for(int i = 0; i < 24; i++) // for each of the 24 power values putInt(P[i],F); // store the average power fclose(F); // close the landshare file // BATTERY TEST DATA [SEPARATE FILE SO IT CAN BE UPDATED INDEPENDENTLY] int Q[24] = { // Specific gravity of each cell [SGUs, density of water = 1000] 1200,1215,1188,1209,1219,1197, // for the Left-Hand Bus 1207,1216,1220,1193,1215,1212 // for the Right-Hand Bus }, R[24] = { // Cell acid temperature in tenth of degrees Celcius 370,353,367,374,381,383, // for the Left-Hand Bus 365,376,353,374,377,366 // for the Right-Hand Bus }; F = fopen("data/batt.dat","w"); // open the battery data file for(int i = 0; i < 12; i++) { // for each of the 12 battery cells putInt(Q[i],F); // store the specific gravity putInt(R[i],F); // store the temperature } fclose(F); // close the battery file // CONSUMPTION TEST DATA [SEPARATE FILE SO IT CAN BE UPDATED INDEPENDENTLY] int U[24] = {0,27,36,39,45,147, // tenths of megajoules per day 0,26,33,41,44,144}, V[24] = {0, 9,12,13,15,49, // percentage power consumption 0,11,14,14,14,47}; F = fopen("data/cons.dat","w"); // open the consumption data file for(int i = 0; i < 12; i++) { // for each of the 12 battery cells putInt(U[i],F); // store the specific gravity putInt(V[i],F); // store the temperature } fclose(F); // close the battery file } /* CONVERT SECONDS TO HRS:MIN:SEC STRING */ char *showTime(int t, int w) { static char S[9]; if(w == 0) // if unsigned output is requested S[0] = ' '; // begin with space else // otherwise, signed output is required if(t < 0) { // so if the presented time is negative S[0] = '-'; // begin with a minus sign t = -t; // and make the time figure positive } else // if the presented time is positive S[0] = '+'; // begin with a plus sign int s = t % 60, // remaining seconds x = t / 60, // total whole minutes m = x % 60, // remaining minutes h = x / 60; // total whole hours if(h < 10) S[1] = '0'; else S[1] = h / 10 + 48; S[2] = h % 10 + 48; S[3] = ':'; if(m < 10) S[4] = '0'; else S[4] = m / 10 + 48; S[5] = m % 10 + 48; S[6] = ':'; if(s < 10) S[7] = '0'; else S[7] = s / 10 + 48; S[8] = s % 10 + 48; return S; } /* COMPUTE "EQUATION OF TIME" FOR TODAY: an empirical solar time off-set [in seconds]. Corrects for perturbations in position of sun caused by eccentr- icity of Earth's orbit and Earth's axial tilt. Ranges from -15 to +17 min- utes. The constants below will serve well for decades but must eventually be adjusted. Called once a day just after midnight by T3sunTime(). NOTES: winter solstice first midday of new year | | (1) Period from 15:58 21Dec2021 to 12:00 01Jan2022 = 10.834722222 days (2) Period from 12:00 01Jan2022 to 06:52 04Jan2022 = 2.786111 days | | first midday of new year perihelion */ void T3EoT() { // Earth's mean orbital angular velocity #define OV 0.017202791 /* 2 * pi / 365.2422 radians per day Orbital angle [in radians] that the Earth would have swept at its average angular speed from last Northern winter solstice to today | Rate the Earth moves around its orbit [in radians per day] | | Current number of completed days since 01 January | | | days from the winter solstice to 01 January (1) | | | | */ double A = OV * (e3 + 10.834722222); /* in radians <-------|-------> Elapsed time [in days] since the last Northern winter solstice. Angle Earth has moved around its orbit since last winter solstice | First-order correction for Earth's orbital eccentricity, | 0.0167 multiplied by 2 to give half-turns [half-days]. | | Days from 01 Jan to Earth's perihelion on 04 Jan (2) | | | */ double B = A + 0.0334 * sin(OV * (e3 - 2.786111)); /* in radians <-----|-----> Elapsed time [in days] since the most recent perihelion. Reciprocal of cos(0.41) [0.41 radians is the tilt of the Earth's axis] | */ double C = (A - atan(1.089945412 * tan(B))) / pi; /* number of seconds To determine which quadrant applies in half a day | to the result of the 'atan' function. | <-------|-------> */ t3 = (int)round(43200 * (C - (int)(C + 0.5))); // seconds of time. // B is also the Sun's ecliptic longitude [shifted by pi], so: c3 = asin(sin(0.41) * cos(B)); // solar declination [in radians] if(b1 < 0) c3 = -c3; // reverse sign V429810-2 for Southern Hemisphere } /* COMPUTE THE SINE OF THE SOLAR ELEVATION AT THE CURRENT 5-MINUTE PERIOD 'p' OF THE CURRENT DAY. Called only once from near end of T3showSolar(), which is executed once every 5 minutes. */ double T3solElev(int p) { /* rotation per 5-minute period 2pi / 288 [radians] | current 5-minute period of the day [sun time] | | half the number of 5-minute periods per day | | | */ double h = 0.021816616 * (p - 144); /* HOUR ANGLE [in radians] Compute SINE OF SOLAR ELEVATION at current time of day. Gives proportion of solar power per unit area hitting a flat horizontal photovoltaic array. | | declination latitude [in radians] hour angle | | | | | */ double a = sin(c3) * sin(l3) + cos(c3) * cos(l3) * cos(h); double x = asin(a); // solar angle of elevation [in radians] x3 = (int)(x / RPD); /* solar angle of elevation [in degrees] Taking the sine again attempts to simulate the attenuation of solar power according to how much atmosphere it must traverse at different times of day. Use 'a' not 'sin(a)' for a fully steered array tracking the sun continually. | | maximum PV output is, say, 5000 watts | | */ w3 = (int)(sin(a) * 5000); /* simulated photovoltaic output [in watts] COMPUTE THE SOLAR AZIMUTH IN DEGREES [ALL INPUT ANGLES IN RADIANS] | angle of declination angle of elevation | | latitude hour angle | | | | | | */ d3 = (int)(acos((sin(c3)*cos(l3) - cos(c3)*sin(l3)*cos(h))/cos(x)) / RPD); if(h > 0) d3 = 360 - d3; // need complementary angle for afternoon } void T3showGraph(int n) { // 'n' is the landshare record number XSetForeground(XD,XG,BLACK); XFillRectangle(XD,XW,XG,H3,V3,290,201); // CLEAR THE GRAPH AREA // DRAW THE GRATICULE int h = H3, // horizontal start-point for the vertical graticule lines v = V3 + 200, // vertical end-point for vertical graticule lines i; XSetForeground(XD,XG,DARK); for(i = 0; i < 25; i++) { // for each of the vertical graticule lines XDrawLine(XD,XW,XG,h,V3,h,v); h += 12; // move across to next line position } v = V3; // vertical start position of horizontal graticule lines h = H3 + 288; // end-point of horizontal graticule lines for(i = 0; i < 11; i++) { // for each of the horizontal graticule lines XDrawLine(XD,XW,XG,H3,v,h,v); v += 20; // move down to next line position } // DRAW THE HORIZONTAL SCALE XSetForeground(XD,XG,GREY); v = V3 + 203; // vertical position of the horizontal scale axis XDrawLine(XD,XW,XG,H3,v,h,v); // horizontal scale axis XDrawString(XD,XW,XG,H3+67,V3+235,"Hour of the Day [Sun Time]",26); h = H3 - 6; // horizontal start position of the horizontal scale figures int q = v, // vertical position of start-point of scale mark p = q + 7, // vertical position of end-point of scale mark r = h + 6, // initial horizontal position of the horizontal scale marks s = 0; // scale figure [00,03,06,09,12,15,18,21,24] v = V3 + 220; // vertical position of base-line of scale figures for(i = 0; i < 9; i++) { // for each of the horizontal scale figures XDrawString(XD,XW,XG,h,v,showInt(s,2,1),2); XDrawLine(XD,XW,XG,r,p,r,q); // horizontal scale marks h += 36; // move across to next scale figure r += 36; // move across to next scale mark s += 3; // increment to next figure to be displayed } // DRAW THE VERTICAL SCALE h = H3 - 3; // horizontal position of the vertical scale axis v = V3 + 200; // vertical end-point of the vertical scale axis XDrawLine(XD,XW,XG,h,V3,h,v); // draw the vertical scale axis h = H3 - 35; // horizontal start of scale figures v = V3 + 5; // vertical base of scale figures p = H3 - 3; // horizontal start position of scale mark q = H3 - 8; // horizontal end position of scale mark r = V3; // initial vertical position of scale mark s = 5000; // scale figure [00 to 12] XDrawString(XD,XW,XG,h,V3-15,"Watts",5); for(i = 0; i < 11; i++) { // for each of the vertical scale figures XDrawString(XD,XW,XG,h,v,showInt(s,4,0),4); XDrawLine(XD,XW,XG,p,r,q,r); // horizontal scale marks v += 20; // drop down to next scale figure r += 20; // drop down to next scale mark s-=500; // decrement to next scale figure to be displayed } } /* DISPLAY THE WIND GAIN GRAPH. Shows the wind gain in kilowatts throughout the day so far. Called by every 5 minutes by T3sunTime(). */ void T3showOric(int n) { T3showGraph(n); // display the blank graph int h = H3 + p3, // start position of graph v = V3 + 200, // vertical position of bottom of yellow bar i, p; XSetForeground(XD,XG,GREEN); XDrawString(XD,XW,XG,H3+50,V3-15,"ELECTRICAL POWER GAIN FROM WIND",31); XSetForeground(XD,XG,BLU); XDrawLine(XD,XW,XG,h,V3,h,v); // time period line for this 5-min period h = H3; XSetForeground(XD,XG,GRN); // colour for wind-power bars on graph int y, E = 0; // energy gained so far today double x = 0.01; for(i = 0; i < p3; i++) { // for each 5-minute period so far today p = 3500 - x * 5000; /* wind power [in watts] vertical position of the horizontal base line of the graph | wind power output [in kW] | | */ y = v - p / 30; // height of vertical yellow bar above graph base line if(p > 0) { // provided the result is above zero XDrawLine(XD,XW,XG,h,y,h,v); // vertical line for this 5-min period E += p * 300; // add energy [in joules] for this 5-min period } h++; // move across to the next vertical line's position x = 3.68 * x * (1 - x); // compute next random wind value } v = 178; XSetForeground(XD,XG,BLACK); // black background for clearing XFillRectangle(XD,XW,XG,83,v+=20,20,15); // clear period number XFillRectangle(XD,XW,XG,53,v+=70,38,15); // clear the power output XFillRectangle(XD,XW,XG,53,v+=20,56,15); // clear the energy v = 190; XSetForeground(XD,XG,BLEEN); // green background for clearing XDrawString(XD,XW,XG,83,v+=20,showInt(p3,3,0),3); // period number v += 50; XDrawString(XD,XW,XG,53,v+=20,showInt(p,6,0),6); // current power output XDrawString(XD,XW,XG,53,v+=20,showInt(E/1000,9,0),9); // energy kJ } /* DISPLAY THE SOLAR GAIN GRAPH. Shows the solar gain in kilowatts throughout the day so far. Called by every 5 minutes by T3sunTime(). */ void T3showSolar(int n) { T3showGraph(n); // display the blank graph int h = H3 + p3, // start position of graph v = V3 + 200, // vertical position of bottom of yellow bar i; XSetForeground(XD,XG,YELLOW); XDrawString(XD,XW,XG,H3+26,V3-15, "ELECTRICAL POWER GAIN FROM SOLAR PANELS",39); XSetForeground(XD,XG,BLU); XDrawLine(XD,XW,XG,h,V3,h,v); // time period line for this 5-min period h = H3; XSetForeground(XD,XG,YELL); // colour for sun-power bars on graph double w, z; int y, E = 0; // energy gained so far today for(i = 0; i < p3; i++) { /* for each 5-minute periods so far today number of the 5-minute period of the day counting from midnight | */ T3solElev(i); /* compute solar geometry and power output of PV array vertical position of the horizontal base line of the graph | PV output in watts [computed by T3solElev() above] | | 25 watts per vertical pixel | | | */ y = v - w3 / 25; // height of vertical yellow bar above graph base line if(w3 > 0) { // provided the result is above zero XDrawLine(XD,XW,XG,h,y,h,v); // vertical line for this 5-min period E += w3 * 300; // add energy [in joules] for this 5-min period } h++; // move across to the next vertical line's position } v = 78; XSetForeground(XD,XG,BLACK); // black background for clearing XFillRectangle(XD,XW,XG,83,v+=20,60,74); // clear solar diff etc. XFillRectangle(XD,XW,XG,83,v+=80,20,35); // clear day number XFillRectangle(XD,XW,XG,83,v+=50,20,35); // clear the solar elev & brng XFillRectangle(XD,XW,XG,53,v+=40,38,15); // clear the power output XFillRectangle(XD,XW,XG,53,v+=20,56,15); // clear the energy v = 90; XSetForeground(XD,XG,BLEEN); // display the sun's current elevation XDrawString(XD,XW,XG,83,v+=20,showTime(s3-q3,1),9); // Solar Diff XDrawString(XD,XW,XG,83,v+=20,showTime(u3,1),9); // GMT off-set XDrawString(XD,XW,XG,83,v+=20,showTime(t3,1),9); // EoT offset XDrawString(XD,XW,XG,83,v+=20,showTime(t3+u3,1),9); // Total offset XDrawString(XD,XW,XG,83,v+=20,showInt(e3+1,3,0),3); // day number today XDrawString(XD,XW,XG,83,v+=20,showInt(p3,3,0),3); // period number XDrawString(XD,XW,XG,83,v+=30,showInt(x3,3,1),3); // solar elevation XDrawString(XD,XW,XG,83,v+=20,showInt(d3,3,1),3); // solar bearing XDrawString(XD,XW,XG,53,v+=20,showInt(w3,6,0),6); // current power output XDrawString(XD,XW,XG,53,v+=20,showInt(E/1000,9,0),9); // energy kJ } /* COMPUTE & DISPLAY CLOCK-TIME & TRUE LOCAL SUN-TIME. A day contains 86400 seconds of time. A circle contains 1296000 seconds of arc. So Earth rotates 15 seconds of arc per second of time. Called from timing loop in main(). */ void T3sunTime(int w) { et = time(NULL); // time in seconds since 00:00:00 UDT 01 Jan 1970 tm = *localtime(&et); // get the local time parameters e3 = tm.tm_yday; // current day number within year [staring at day 0] /* provided we're currently in the POWER tab AND either the | SOLAR sub-tab is visible OR the | | WIND sub-tab is visible | | | */ if(tb == 3 && (a3 & 2 || a3 & 4)) { static int S = 0; // second number from previous pass if(w == 0) { // if a normal timed pass int s = tm.tm_sec; // current second number [clock time] if(S == s) return; // current second not yet expired S = s; // note new second number for next pass } static int D = 0; // day number from previous pass if(D != e3 || w == 1) { // if day num changed or is an initial display pass D = e3; // note the day number for the next pass T3EoT(); // re-do the daily computations } XSetForeground(XD,XG,BLACK); // background colour XFillRectangle(XD,XW,XG,83,57,55,37); // clear the time display area XSetForeground(XD,XG,BLEEN); // colour for time displays // local official CLOCK TIME in seconds since midnight q3 = (int)(((tm.tm_hour * 60) + tm.tm_min) * 60 + tm.tm_sec); // display the local CLOCK TIME [according to official time zone] XDrawString(XD,XW,XG,83,70,showTime(q3,0),9); /* cast to 'int' because 'et' is a 'time_t' type | seconds since 01 Jan 1970 00:00:00 GMT PGBR7A3FB79 | | get remainder [seconds so far today] after dividing by | | | the number of seconds per day 016.815.226-67 | | | | */ s3 = (int)(et % 86400) + u3 + t3; /* SECONDS SINCE MIDNIGHT LOCAL SUN TIME | | | equation of time [in seconds of time] landshare's longitudinal time off-set [seconds] */ // display the fully corrected SUN TIME at the landshare location XDrawString(XD,XW,XG,83,90,showTime(s3,0),9); p3 = s3 / 300; // number of the current 5-minute sun period of the day /* THE 5-MINUTE [300 SECOND] TIMER: if at the start of a new 5-minute solar period or its an initial display pass: update the solar graph. */ if(s3 % 300 == 0 || w == 1) { if(a3 & 2) T3showSolar(1); else if(a3 & 4) T3showOric(1); } } // close of entry condition } /* DISPLAY THE IN-FILL FOR THE 'SOLAR' DISPLAY OF THE 'POWER' TAB */ void T3showWind() { clearTab(); // clear the tab area showMsg(0); // show the blank message area int V = 70, v = 50; XSetForeground(XD,XG,GREY); XDrawString(XD,XW,XG,17,v+=20,"CLOCK TIME",10); XDrawString(XD,XW,XG,17,v+=20,"SOLAR TIME",10); v+=100; XDrawString(XD,XW,XG,17,v+=20,"PERIOD NUM",10); v+=50; XDrawString(XD,XW,XG,17,v+=20,"POWER WATTS",18); XDrawString(XD,XW,XG,17,v+=20,"ENERGY kJ",18); T3sunTime(1); // 1 = force immediate execution of the 5-minute functions T3Buts(); // display the states of the control buttons } /* DISPLAY THE IN-FILL FOR THE 'SOLAR' DISPLAY OF THE 'POWER' TAB */ void T3showSun() { clearTab(); // clear the tab area showMsg(0); // show the blank message area int V = 70, v = 50; XSetForeground(XD,XG,GREY); XDrawString(XD,XW,XG,17,v+=20,"CLOCK TIME",10); XDrawString(XD,XW,XG,17,v+=20,"SOLAR TIME",10); XDrawString(XD,XW,XG,17,v+=20,"SOLAR DIFF",10); XDrawString(XD,XW,XG,17,v+=20,"GMT OFFSET",10); XDrawString(XD,XW,XG,17,v+=20,"EoT OFFSET",10); XDrawString(XD,XW,XG,17,v+=20,"TOT OFFSET",10); XDrawString(XD,XW,XG,17,v+=20,"DAY NUMBER",10); XDrawString(XD,XW,XG,17,v+=20,"PERIOD NUM",10); XDrawString(XD,XW,XG,17,v+=30,"SOLAR ELEV DEG",18); XDrawString(XD,XW,XG,17,v+=20,"SOLAR BRNG DEG",18); XDrawString(XD,XW,XG,17,v+=20,"POWER WATTS",18); XDrawString(XD,XW,XG,17,v+=20,"ENERGY kJ",18); T3sunTime(1); // 1 = force immediate execution of the 5-minute functions T3Buts(); // display the states of the control buttons } /* ONE OF THE TAB'S CONTROL BUTTONS WAS CLICKED. Button numbers: 0:SCAN, 1:STOP, 2:STDIN, 7:HELP. a3 = Tab 3 Button States. Buttons are numbered 0 to 7 from left to right. 'y' has a '1' in the position in the lower 8 bits of b1 corresponding to the number of the clicked button as follows: [Called from 1 place in MEH().] */ void T3horizButClick(int x) { a3 &= ~0x40; // kill the REGEN button switch(x) { // switch according to button number case 0: // SOURCE button if(!(a3 & 1)) { // if the SOURCE button is bright a3 = 0; // dim all buttons a3 |= 1; // make SOURCE button bright T3source(1); // show landshare record 1 } break; case 1: if(!(a3 & 2)) { // if the SOLAR button is bright a3 = 0; // dim the other buttons a3 |= 2; // make SOLAR button bright T3showSun(); // show SOLAR gain for record 1 } break; case 2: if(!(a3 & 4)) { // if the WIND button is bright a3 = 0; // dim all buttons a3 |= 4; // make WIND button bright T3showWind(); // show solar gain for record 1 } break; case 3: if(!(a3 & 8)) { // if the APU button is bright a3 = 0; // dim buttons a3 |= 8; // make APU button bright T3apu(1); // show APU situation } break; case 4: // BATT if(!(a3 & 0x10)) { // if the BATT button is not bright a3 = 0; // clear all buttons a3 |= 0x10; // brighten it T3batt(); // display the battery sub-tab } break; case 5: // CONSUM if(!(a3 & 0x20)) { // if the CONSUM button is not bright a3 = 0; // clear all buttons a3 |= 0x20; // brighten it T3cons(); // display the CONSUMPTION sub-tab } break; case 6: // REGEN button if(!(a3 & 0x40)) { // if REGEN button not bright a3 &= 0; // clear all buttons a3 |= 0x40; // brighten the REGEN button T3regen(1); // regenerate the power test data for landshare 1 } break; case 7: // HELP button was clicked if(!(a3 & 0x80)) { // if HELP button not bright a3 &= 0; // clear all buttons a3 |= 0x80; // brighten the HELP button helpPage("software/fep.html#power"); } } T3Buts(); // re-display the buttons } void T3click() { /* not used at the moment */ } /* SETS UP THE HEADINGS AND BUTTONS FOR THE POWER TAB. Re-displays the current command lines for a SCAN that is still active. Called only from 1 place in showTab(). */ void T3show() { if(a3 & 2) T3showSun(); // if SOLAR button bright, display SOLAR sub-tab else if(a3 & 4) T3showWind(); // if WIND button bright, display WIND sub-tab else if(a3 & 8) T3apu(1); // if APU button bright, display APU sub-tab else if(a3 & 16) T3batt(1); // if BATT button bright, display APU sub-tab else if(a3 & 32) T3cons(1); // if CONS button bright, display APU sub-tab else { a3 |= 1; // else press SOURCE button T3source(1); // and force display of the SOURCE sup-tab } T3Buts(); // display the control buttons } // FUNCTIONS PERTAINING TO TAB 2: WATER -------------------------------------- /* DRAW THE HORIZONTAL ROW OF CONTROL BUTTONS ACROSS THE BOTTOM OF THE WATER TAB. Called from 1 place each in T2showGraph(), T2showRainTank(), T2horizButClick(x) and T2show(). */ void T2showButs() { if(tb != 2) return; showButs(); // show 8 blank buttons across the bottom of the tab area const char // annotate the horizontal row of control buttons *A[] = {"RAIN","WELL","LAKE","STEAM","COND","MAIN","RECYC","HELP"}; const int a[] = {15,15,15,12,15,15,12,15}, // horizontal offsets for word starts b[] = { 4, 4, 4, 5, 4, 4, 5, 4}; // number of letters in each word int h = 17, c; // left edge of left-most button for(int i = 0; i < 8; i++) { // for each of the 8 buttons: if(a2 & 1 << i) // if this button is 'on' c = WHITE; // 'on' button is white else c = GREY; // if this button is off, make it grey XSetForeground(XD,XG,c); // colour for the lettering XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]); h += 59; // move across to next button } } /* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE RAIN SUB-TAB. Called from T2showGraph(), T2showRainGraph(), T2showRainTank() */ void T2showGraphVertButs() { char // annotate the vertical row of control buttons *A[] = {"mm","m3","TANK","SPARE","SPARE","SPARE","SPARE","SPARE"}; int a[] = {21,21,15,12,12,12,12,12}, // horizontal offsets for word starts b[] = { 2, 2, 4, 5, 5, 5, 5, 5}, // number of letters in each word h = 93; // y coordinate of button block for(int i = 0; i < 8; i++) { // for each of the 8 buttons: XSetForeground(XD,XG,DARK); // shade for buttons' background XFillRectangle(XD,XW,XG,17,h-15,53,21); if(v2 & 1 << i) // if this button is bright [on] XSetForeground(XD,XG,WHITE); // set colour for bright lettering else // otherwise XSetForeground(XD,XG,GREY); // set colour for dull lettering XDrawString(XD,XW,XG,17+a[i],h,A[i],b[i]); h += 27; // move across to the next button } } /* YEAR-GRAPH: draw a histogram graph of the water captured per day. This function is called by each of the sub-tabs of the WATER tab. Called only from: T2showRainGraph(m). */ void T2showGraph(int scale) { // DRAW THE GRATICULE int h = H2 - 15, // horizontal start-point for the vertical graticule lines v = V2 + 200, // vertical end-point for vertical graticule lines m[] = {31,28,31,30,31,30,31,31,30,31,30,31}, n[] = {7,6,7,7,7,6,7,7,7,8,7,7}, i; clearTab(); XSetForeground(XD,XG,DARK); for(i = 0; i < 13; i++) { // for each of the vertical graticule lines XDrawLine(XD,XW,XG,h,V2,h,v); h += m[i]; // move across to next line position } v = V2; // vertical start position of horizontal graticule lines h = H2 + 350; // end-point of horizontal graticule lines for(i = 0; i < 11; i++) { // for each of the horizontal graticule lines XDrawLine(XD,XW,XG,H2-15,v,h,v); v += 20; // move down to next line position } // DRAW THE HORIZONTAL SCALE XSetForeground(XD,XG,GREY); v = V2 + 203; // vertical position of the horizontal scale axis XDrawLine(XD,XW,XG,H2-15,v,h,v); // horizontal scale axis h = H2 - 15; // horizontal start position of the horizontal scale figures int q = v, // vertical position of start-point of scale mark p = q + 7, // vertical position of end-point of scale mark r = h + 6; // initial horizontal position of the horizontal scale marks v = V2 + 220; // vertical position of base-line of scale figures char *S[] = {"JAN","FEB","MAR","APR","MAY","JUN", "JUL","AUG","SEP","OCT","NOV","DEC",""}; for(i = 0; i < 13; i++) { // for each of the horizontal scale figures XDrawLine(XD,XW,XG,h,p,h,q); // horizontal scale marks if(i < 13) XDrawString(XD,XW,XG,h+n[i],v,S[i],3); h += m[i]; // move across to next month name r += m[i]; // move across to next scale mark } // DRAW THE VERTICAL SCALE h = H2 - 18; // horizontal position of the vertical scale axis v = V2 + 200; // vertical end-point of the vertical scale axis XDrawLine(XD,XW,XG,h,V2,h,v); // draw the vertical scale axis h = H2 - 50; // horizontal start of scale figures v = V2 + 5; // vertical base of scale figures p = H2 - 18; // horizontal start position of scale mark q = H2 - 23; // horizontal end position of scale mark r = V2; // initial vertical position of scale mark int s = 100, // maximum figure on vertical scale ds = 5, // increment in the actual scale figure dv = 20, // pixel rise between vertical scale figures dr = 20, // vertical rise to next higher scale mark I = 11; // Number of iterations in the for() loop below XSetForeground(XD,XG,YELLOW); if(v2 & 1) { // if the vertical WATER button is bright XDrawString(XD,XW,XG,h+36,V2-15, "MILLIMETRES OF RAIN FOR EACH DAY OVER THE LAST 12 MONTHS",56); switch(scale) { // set the parameters for the graph scale case 1: ds = 10; break; case 2: s = 50; break; case 3: I = 6; s = 25; dv = 40; dr = 40; } } /* We need to convert millimetres of rain to litres of water collected. One millimetre of rain falling on 1 square meter of roof = 1 litre of water. The UTD's roof has a collecting area of 400 square metres. So the UTD's roof yields 400 litres of water for every millimetre of rain. We do this conversion simply by changing the vertical scale figures on the graph, which is given in cubic metres instead of litres. */ else { XDrawString(XD,XW,XG,20,V2-15,"CUBIC METRES OF RAIN WATER " "PER DAY COLLECTED FROM THE UTD'S 400 SQ METRE ROOF",77); s = 40; // maximum figure on vertical scale ds = 2; // increment in the actual scale figure switch(scale) { // set the parameters for the graph scale case 1: ds = 10; break; case 2: s = 20; break; case 3: I = 6; s = 10; dv = 40; dr = 40; } } XSetForeground(XD,XG,GREY); for(i = 0; i < I; i++) { // for each of the vertical scale figures XDrawString(XD,XW,XG,h,v,showInt(s,4,0),4); XDrawLine(XD,XW,XG,p,r,q,r); // horizontal scale marks v += dv; // drop down to next scale figure r += dr; // drop down to next scale mark s -= ds; // decrement to next scale figure to be displayed } T2showButs(); T2showGraphVertButs(); showMsg(0); } /* The following 7 sub-tabs need a common graphical sub-routine function to display 366 days of water quantities. Rain water is the main supply. Well, lake, stream, condenser and water main make up the shortfall of rain. Household consumes 227 litres of water per person per day, which is 82855 litres per person per year. The UTD supports up to 4 people, so its water consumption would be 331,420 litres per year. Assume a rainfall of 1 metre per year. Each square metre of roof horizontal area yields 1 cubic metre of water per year. The UTD roof has a collecting area of 400 square metres. It can therefore collect 400 cubic metres of water per year. That is 400,000 litres per year. This means the rainfall would supply all the UTD's water with 17% to spare for evaporation and leakage. To account for a long dry season, storage for about 200 cubic metres of water is required. This would need to be aerated continually. Rain supplies 1096 litres per day on average. Record rainfall in a single day in Belo Horizonte = 171,8 millimetres. Make the scale up to 200 mm. Put vertical buttons to get separate mm graph, litre graph and tabulated figures displays. Called only from: T2showRain(). */ void T2showRainGraph() { int // rain each month in millimetres r[] = { 27,25,23,13,20,15,7,10,5,8,10,13,12,9,14, // January 331 5,7,12,11,5,10,12,10,5,3,5,5,13,10,7,6, 6,7,9,10,8,6,4,5,5,3,4,5,7,9,7, // February 178 5,7,9,10,8,7,9,9,7,3,4,5,6, 6,7,9,10,8,6,4,5,5,3,4,5,7,9,7, // March 198 5,7,9,10,8,7,9,9,7,3,4,5,6,7,6,5, 3,2,3,2,3,3,2,1,3,2,2,3,3,2,3, // April 82 2,4,3,5,4,3,4,4,3,1,2,3,3,4,3, 2,1,0,1,0,2,1,0,0,0,0,1,0,0,1, // May 28 2,0,1,0,0,2,2,0,2,1,0,3,3,0,3,1, 0,1,0,0,2,0,0,3,0,0,0,0,0,0,1, // June 11 2,0,1,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0, // July 5 2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,1,0,0,2,0,0,3,0,0,0,0,0,0,1,0, // August 11 2,0,1,0,0,0,0,0,0,0,0,0,0,0,0, 0,1,0,0,4,0,0,6,0,0,0,0,3,0,2, // September 49 2,0,4,0,0,0,0,0,0,4,5,6,7,5,0, 0,5,6,7,5,3,7,6,0,0,9,0,3,0,5,0, // October 110 2,3,4,0,0,0,0,7,6,9,5,6,7,5,9, 2,3,4,0,0,0,0,7,6,9,5,6,7,5,9, // November 236 10,15,16,17,15,13,17,16,12,0,9,5,3,0,5, 12,13,14,0,0,0,0,12,11,10,12,15,14,13,11,9, // December 339 11,12,13,11,13,13,17,16,12,0,9,15,18,20,25}, /* In an implemented system, the above data would come from a file maint- ained by a daemon that is constantly updating daily rainfall levels. */ h = H2 - 15, // horizontal start-point for the vertical graticule lines v = V2 + 200, // vertical end-point for vertical graticule lines i, // general loop index variable x, // current day's rainfall figure in millimetres y = 0, // to capture the highest daily rainfall for the year m = 1; // graph scale 1 = 0 to 100, 2 = 0 to 50, 3 = 0 to 25 for(i = 0; i < 365; i++) // find the highest rainfall for any day if(y < (x = r[i])) y = x; // to facilitate automatic graph scaling /* If the highest rainfall figure is less than quarter-way up the 0 to 100 scale, quadruple the scale to 0 to 25; else if it is less than half-way, double the scale to 0 to 50. */ if(y < 25) m = 3; else if(y < 50) m = 2; T2showGraph(m); // display the scale and graticule XSetForeground(XD,XG,YELLOW); XDrawLine(XD,XW,XG,h + e3,v,h + e3,v - 200); // day of the year bar XDrawString(XD,XW,XG,h + 60,v + 38, "The yellow vertical line indicates today.",41); for(i = 0; i < 365; i++) { // for each of the vertical graticule lines XSetForeground(XD,XG,GREEN); if((x = r[i]) > 100) // if the rainfall for the day is off-scale XSetForeground(XD,XG,RED); // display the line in red XDrawLine(XD,XW,XG,h,v,h,v - (x << m)); h++; // move across to next line position } T2showGraphVertButs(); } /* Need to store enough rain water for the dry season. Total rainwater coll- ected during a year is 400 cubic metres. The dry season lasts 180 days. Therefore we need to store half of the annual collection. Total annual collection is 1 metre of rain times 400 square metres of roof area = 400 cubic metres [400000 litres] of rain water. Consumption is up to 1000 litres per day [including local crop watering]. To span the dry season, we need to store, over the rainy season, 1000 x 180 days = 180000 litres of water. For contingency, make the tank capacity 200000 litres. That is 200 cubic metres. The cube root of 216 is 6. The tank could have a height of 6 metres with a base area of 36 square metres. A tank of radius 3385 mm and 6 metres high would be needed. This would be a cylinder of almost minimum possible surface area for the volume contained. It could be implemented as an underground plastic tank set in a pit lined with concrete. The water in the tank must be kept aerated. Called from: T2graphVertButs(x), T2showRain(). */ void T2showRainTank() { // display the water collected graph int V[] = { 1000, // A annual rainfall in millimetres 400, // B catchment area in square metres 0, // C total rain capture in litres 180, // D length of dry season in days 0, // E storage needed in litres 200000, // F total capacity of the water tank in litres 192000, // G maximum content recorded in litres 28000, // H minimum content recorded in litres 160000, // I current content in litres 1000, // J consumption in litres per day 0 // K number of days water supply stored in tank }, s[] = {35,37,36,28,36,30,30,30,30,34,34}, // annotation string lengths X = 220, // horizontal start position of the data block Y = 93, // vertical datum of top line of data block v = Y, // vertical coordinate of current data line l = 19, // inter-line spacing within data block i = 0; // loop variable char *S[] = { "A ANNUAL RAINFALL MILLIMETRES", "B CATCHMENT AREA SQUARE METRES", "C ANNUAL CAPTURE LITRES [AxB]", "D DRY SEASON DAYS", "E STORAGE NEEDED LITRES [DxJ]", "F TANK CAPACITY LITRES", "G CONTENT MAX LITRES", "H CONTENT MIN LITRES", "I CONTENT NOW LITRES", "J CONSUMPTION LITRES/DAY", "K SURVIVAL TIME DAYS [I/J]" }; V[ 2] = V[0] * V[1]; // rain captures = annual rainfall x roof area V[ 4] = V[3] * V[9]; // required storage = dry days x daily consumption V[10] = V[8] / V[9]; // survival time = current reserve / daily consumption clearTab(); XSetForeground(XD,XG,YELLOW); XDrawString(XD,XW,XG,17,65,"RAIN WATER COLLECTION",21); XSetForeground(XD,XG,BLEEN); XDrawString(XD,XW,XG,17,314, "Rain collection surface, rain ducts & filter cleaning due in: days." ,71); XSetForeground(XD,XG,WHITE); XDrawString(XD,XW,XG,389,314,"132",3); XSetForeground(XD,XG,BLEEN); for(i = 0; i < 11; i++) { // for each data value XDrawString(XD,XW,XG,X,v,S[i],s[i]); v += 19; // drop down to next data line } XSetForeground(XD,XG,WHITE); v = Y; // vertical coordinate of top line of data block for(i = 0; i < 11; i++) { // for each data value XDrawString(XD,XW,XG,321,v,showInt(V[i],6,0),6); v += 19; // drop down to next data line } XSetForeground(XD,XG,YELLOW); XDrawString(XD,XW,XG,119,Y+42,"TANK",4); T3percent(V[8]*100/V[5],109,Y+51); // show % level in rain tank T2showGraphVertButs(); T2showButs(); showMsg(0); // clear the message line } /* DISPLAYS THE RAIN SUB-TAB OF THE WATER TAB. Which of the three displays is shown is determined by which of the sub-tab's vertical buttons is bright. Called from: T2graphVertButs(x), T2horizButClick(x), T2show() */ void T2showRain() { if(v2 & 1 // if the 'mm' [millimetres of rain] button || v2 & 2) // or the 'm3' [cubic metres] button is bright T2showRainGraph(); // display the appropriate annual graph else if(v2 & 4) // else if the 'DATA' button is bright T2showRainTank(); // display the Rain Data with tank percentage graphic } /* DISPLAYS THE 'WELL' TAB. Called from: T2show(), T2horizButClick(). */ void T2showWell() { clearTab(); XSetForeground(XD,XG,YELLOW); // colour for the title XDrawString(XD,XW,XG,17,70,"WATER DRAWN FROM WELL",21); XSetForeground(XD,XG,BLEEN); // colour for the lettering char *S[] = { "In areas where rainfall is insufficient, water can be drawn from a ", "narrow gauge bore hole. The UTD has an integral small-bore well drill", "as standard equipment for this purpose. Flushing and washing water is", "re-cycled. Only drinking water need be drawn from the bore hole then ", "filtered and tested for potability. Purifying can be done by passing ", "the water very slowly through a horizontal glass pipe half full of ", "air while fully exposed to strong sunlight. " }; int h = 50, v = 120; for(int i = 0; i < 7; i++) { XDrawString(XD,XW,XG,h,v,S[i],69); v += 17; } showMsg(0); // clear the message line // future: graph of water drawn from well each day of the year. } void T2showLake() { clearTab(); XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"WATER PUMPED FROM LAKE",22); XSetForeground(XD,XG,BLEEN); // colour for the lettering char *S[] = { "In areas where rainfall is insufficient, water can be pumped from a ", "small lake within the landshare. Again, flushing and washing water ", "should be recycled independently, only being topped up from the lake.", "The lake supplies drinking water only, which is purified by exposure ", "to strong sunlight. Alternatively, water can be pumped from a large ", "lake within an anthropological community and distributed to indivi- ", "dual landshares as a community project. " }; int h = 50, v = 120; for(int i = 0; i < 7; i++) { XDrawString(XD,XW,XG,h,v,S[i],69); v += 17; } showMsg(0); // clear the message line // water pumped from lake for each day of the year. // this excludes water used as a closed cycle coolant. } void T2showStream() { clearTab(); XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"WATER TAKEN FROM STREAM",23); XSetForeground(XD,XG,BLEEN); // colour for the lettering char *S[] = { "In areas where rainfall is insufficient, water can be pumped from a ", "small stream within the landshare. Again, flushing and washing water ", "should be recycled independently, and only topped up from the stream.", "The stream supplies drinking water only. This is purified by exposure", "to strong sunlight. Alternatively, water can be pumped from a large ", "river within an anthropological community and distributed to indivi- ", "dual landshares as a community project. " }; int h = 50, v = 120; for(int i = 0; i < 7; i++) { XDrawString(XD,XW,XG,h,v,S[i],69); v += 17; } showMsg(0); // clear the message line // water drawn from stream for each day of the year. } /* DRINKING/COOKING WATER AIR CONDENSER SUB-TAB. Called from: T2horizButClick() and T2show(). */ void T2showCond() { int h = 17, // horizontal start coordinate of print line v = 70, // vertical coordinate of base of print line l = 15, // inter-line spacing I = 15, // number of lines of print s[] = {65,53,56,51,60,62,58,48, // number of characters in eacj line 42,42,42,48,59,56,47}, D[] = {4,48,4,37,122,45,33,90,156, // data values for each line 1038,1038,2000,1100,280,0}, d[] = {2,2,2,2,3,3,3,3, // number of digits in each line field 4,4,4,4,4,4,4}; char *S[] = { "ACTIVITY: % Percentage of time the water extractor is active.", "HUMIDITY: % Relative humidity of the outside air.", "CONDENSER: C Temperature of the water condenser coil.", "EXHAUST: C Temperature of the exhaust dry air.", "FILTER 1: days to go before air filter needs cleaning.", "FILTER 2: days to go before water filter needs cleaning.", "ENERGY AV: Mj of energy per litre of water extracted.", "PUMP: days to next water pump service.", "FAN: hours of service to date.", "COMPRESSOR hours of service to date.", "OZONER: hours of service to date.", "TANK SIZE: Capacity of potable water tank.", "TANK NOW: litres of potable water currently in tank.", "DAILY: litres average amount consumed per day.", "SURVIVAL: days reserve of potable water." }; D[14] = D[12]/D[13]; // compute the survival time clearTab(); XSetForeground(XD,XG,YELLOW); // colour for the title XDrawString(XD,XW,XG,h,v, "DRINKING/COOKING WATER AIR CONDENSER [COMPRESSOR TYPE]",54); v = 92; // base height of first data line XSetForeground(XD,XG,BLEEN); // colour for the annotations for(int i = 0; i < I; i++) { // print the data line annotations XDrawString(XD,XW,XG,h,v,S[i],s[i]); v += l; // drop down to next line } h += 72; // horizontal start of data values v = 92; // vertical bas line of first data value XSetForeground(XD,XG,WHITE); // colour for the amounts for(int i = 0; i < I; i++) { // print the data values int c = d[i]; // number of digits in current data value XDrawString(XD,XW,XG,h,v,showInt(D[i],c,0),c); v += l; // drop down to next line } h = 414; // horizontal start of tank graphic v = 178; // vertical start [top] of tank graphic XSetForeground(XD,XG,YELLOW); // colour for tank graphic title XDrawString(XD,XW,XG,h+9,v,"TANK",4); // display tank graphic title T3percent(D[12]*100/D[11],h,v+6); // show % level in rain tank T2showButs(); // display the sub-tab buttons showMsg(0); // clear the message line } void T2showMain() { clearTab(); XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,"WATER FROM PUBLIC WATER MAIN",28); XSetForeground(XD,XG,BLEEN); // colour for the lettering char *S[] = { "This sub-tab only exists for the transition from the present mode of ", "the economy to the new Landshare-based anthropological community mode", "of economy. In the initial stages of the transition, while the means ", "of acquiring water from rain, well, lake, stream or condensation has ", "not yet been implemented, it will be necessary to buy water from an ", "old style water company. " }; int h = 50, v = 120; for(int i = 0; i < 6; i++) { XDrawString(XD,XW,XG,h,v,S[i],69); v += 17; } showMsg(0); // clear the message line // water drawn from the public main for each day of the year. } /* DRINKING WATER PRODUCTION PLUS WATER RE-CYCLING FUNCTION Called from: T2horizButClick() and T2show(). */ void T2showRecycle() { int X = 160, // horizontal start position of the data block Y = 70, // vertical datum of top line of data block v = Y, // vertical coordinate of current data line l = 18, // inter-line spacing within data block i = 0, // loop variable s[] = {28,28,32,30,32,32,28,28,32,32,30}, // text line lengths D[] = {5000,3000,10,146,400,600,3000,546,70,0,95}, // numeric data d[] = {0,0,0,24,0,0,0,0,0,0,24}; // horiz. off-set of data figures char *S[] = { "A Tank capacity: litres", // 00 text lines "B Tank Content: litres", // 01 "C Wastage Rate: litres/day", // 02 "D Maintenance due in: days", // 03 "E From Rain: av. litres/day", // 04 "F From Air: av. litres/day", // 05 "G Tank Capacity: litres", // 06 "H Tank Content: litres", // 07 "I Consumption: litres/day", // 08 "J Reserve supply: days [H/I]", // 09 "K Maintenance due in: days" // 10 }; D[9] = D[7] / D[8]; // reserve supply of drinking water in days clearTab(); XSetForeground(XD,XG,YELLOW); // colour for the titles lettering XDrawString(XD,XW,XG,17,70,"WATER RE-CYCLING",16); XDrawString(XD,XW,XG,X,Y,"GREY WATER",10); XDrawString(XD,XW,XG,X,Y+108,"DRINKING WATER",14); X -= 12; XSetForeground(XD,XG,BLEEN); // colour for the text lines lettering for(i = 0; i < 11; i++) { // display the annotation texts v += l; // drop to first/next line XDrawString(XD,XW,XG,X,v,S[i],s[i]); if(i == 3) v += l + l; // skip blank drop between data sections } X += 102; // add offsets for the majority of data figures v = Y; // reset the vertical displacement back to the top line XSetForeground(XD,XG,WHITE); // colour for the figures for(i = 0; i < 11; i++) { // display the data figures v += l; // drop to first/next line XDrawString(XD,XW,XG,X+d[i],v,showInt(D[i],4,0),4); if(i == 3) v += l + l; // skip blank drop between data sections } // GRAPHIC OF WATER LEVEL IN DRINKING WATER TANK X = 390; // horizontal start of tank graphic Y = 70; // vertical start [top] of tank graphic XSetForeground(XD,XG,YELLOW); // colour for tank graphic title XDrawString(XD,XW,XG,X+9,Y,"TANK",4); // display tank graphic title T3percent(D[1]*100/D[0],X,Y+6); // show % level in rain tank // GRAPHIC OF WATER LEVEL IN GREY WATER TANK X = 41; // horizontal start of tank graphic Y = 176; // vertical start [top] of tank graphic XSetForeground(XD,XG,YELLOW); // colour for tank graphic title XDrawString(XD,XW,XG,X+9,Y,"TANK",4); // display tank graphic title T3percent(D[7]*100/D[6],X,Y+6); // show % level in rain tank T2showButs(); // display the sub-tab buttons showMsg(0); // clear the message line } /* ONE OF THE VERTICAL BUTTONS ON THE RAIN SUB-TAB HAS BEEN CLICKED */ void T2graphVertButs(int x) { switch(x) { case 0: if(!(v2 & 1)) { // if the RAIN button was clicked v2 |= 1; // set the RAIN button bit v2 &= ~254; // clear all the other bits T2showRain(); // display the rain graph } break; case 1: if(!(v2 & 2)) { // if the WATER button was clicked v2 |= 2; // set the WATER button bit v2 &= ~253; // clear all the other bits T2showRain(); // display the water collected graph } break; case 2: if(!(v2 & 4)) { // if the DATA button was clicked v2 |= 4; // set the DATA button bit v2 &= ~251; // clear all the other bits T2showRainTank(); // display the data graph } break; } } /* ONE OF THE XFER TAB'S CONTROL BUTTONS WAS PRESSED. Buttons are numbered 0 to 7 from left to right. [Called from 1 place in MEH().] */ void T2horizButClick(int x) { switch(x) { // switch according to button number case 0: if(!(a2 & 1)) { // if the RAIN button is dull a2 |= 1; // illuminate it a2 &= ~0xFE; // dull all the other buttons T2showRain(); // display the RAIN tab } break; // 00+02+04+08+16+32+64+128=254 case 1: if(!(a2 & 2)) { // if the WELL button is dull a2 |= 2; // illuminate it a2 &= ~0xFD; // dull all the other buttons T2showWell(); // display the WELL tab } break; // 01+00+04+08+16+32+64+128=253 case 2: if(!(a2 & 4)) { // if the LAKE button is dull a2 |= 4; // illuminate it a2 &= ~0xFB; // dull all the other buttons T2showLake(); // display the LAKE tab } break; // 01+02+00+08+16+32+64+128=251 case 3: if(!(a2 & 8)) { // if the STREAM button is dull a2 |= 8; // illuminate it a2 &= ~0xF7; // dull all the other buttons T2showStream(); // display the STREAM tab } break; // 01+02+04+00+16+32+64+128=247 case 4: if(!(a2 & 0x10)) { // if the COND button is dull a2 |= 0x10; // illuminate it a2 &= ~0xEF; // dull all the other buttons T2showCond(); // display the COND tab } break; // 01+02+04+08+00+32+64+128=239 case 5: if(!(a2 & 0x20)) { // if the MAIN button is dull a2 |= 0x20; // illuminate it a2 &= ~0xDF; // dull all the other buttons T2showMain(); // display the MAIN tab } break; // 01+02+04+08+16+00+64+128=223 case 6: if(!(a2 & 0x40)) { // if the TANK button is dull a2 |= 0x40; // illuminate it a2 &= ~0xBF; // dull all the other buttons T2showRecycle(); // display the TANK tab } break; // 01+02+04+08+16+32+00+128=191 case 7: // HELP button was clicked helpPage("software/fep.html#water"); } T2showButs(); // re-display the buttons } /* A CLICK HAS OCCURRED WITHIN TAB 2 OTHER THAN ON ONE OF THE BOTTOM BUTTONS Called from only one place in MEH(). */ void T2click() { int i = 0, y = 77; if(mx < 17 || mx > 69) return; // outside horizontal limits of buttons for(i = 0; i < 8; i++) { if(my > y && my < y + 22) break; y += 27; // drop down to the next button } if(i < 3) T2graphVertButs(i); } /* DISPLAYS THE CONTENT AND BUTTONS FOR THE WATER TAB. Called only from 1 place in showTab(). */ void T2show() { int i; for(i = 0; i < 8; i++) if(a2 & 1 << i) break; switch(i) { case 0: T2showRain(); break; case 1: T2showWell(); break; case 2: T2showLake(); break; case 3: T2showStream(); break; case 4: T2showCond(); break; case 5: T2showMain(); break; case 6: T2showRecycle(); break; } T2showButs(); // display the control buttons } /* FUNCTIONS PERTAINING TO TAB 1: LAND---------------------------------------- /* LOAD THE LANDSHARE'S 2048-BYTE DATA RECORD. The data for each landshare is stored in a 2048-byte record. */ void T1getLand(int n) { // 'n' is the landshare record number FILE *F = fopen("data/land.dat","r"); fseek(F,n << 11,SEEK_SET); // start byte of record [2048 * record number] /* GET THE COORDINATES OF THE LANDSHARE'S CENTRAL REFERENCE POINT A landshare's location is stated as the latitude, longitude and height of its central reference point. */ b1 = getInt(F); /* latitude [seconds of arc] int = 4 bytes latitude of landshare [in seconds of arc] | seconds of arc per radian | | */ l3 = b1 / SPR; // latitude of landshare [in radians] c1 = getInt(F); /* longitude [seconds of arc] int = 4 bytes LONGITUDE of landshare's central reference point [seconds of arc] | 15 seconds of arc per second of time | | */ u3 = c1 / 15; // lanshare's longitudinal time off-set from GMT [seconds] d1 = getShort(F); // height [metres above sea level] short = 2 bytes /* GET RADII FROM CENTRAL REFERENCE POINT TO BOUNDARY. A is mapped in terms of the radial distance [in cm] between its central reference point and its boundary for each of the 360 degrees of bearing. [720 bytes on disk] */ int i; for(i = 0; i < 360; i++) // of each of the 360 degrees of bearing r1[i] = getShort(F); // get the 16-bit radius in centimetres /* GET HEIGHT OF POINT ON BOUNDARY ABOVE OR BELOW CENTRAL REFERENCE POINT The relief of the landshare is expressed as height difference in centi- metres between its central reference point and the point on its boundary for each of the 360 degrees of bearing. Each height is stored as a 16-bit quantity in the land share disk record. [720 bytes on disk] */ for(i = 0; i < 360; i++) { // of each of the 360 degrees of bearing int x = getShort(F); // get the 16-bit height in centimetres h1[i] = 32768 - x; // half full-house bias to make it bipolar } fclose(F); // close the landshare's file } /* STORE THE LANDSHARE'S DATA RECORD. The data for each landshare is stored in a 2048-byte record. lat/lng/hgt 10 bytes radii 720 bytes heights 720 bytes TOTAL 1450 bytes [bytes 0 to 1449 */ void T1putLand(int n) { // 'n' is the landshare record number FILE *F = fopen("data/land.dat","r+"); fseek(F,n << 11,SEEK_SET); // start byte of record [2048 * record number] // PUT THE COORDINATES OF THE LANDSHARE'S CENTRAL REFERENCE POINT putInt(b1,F); // latitude [seconds of arc] 4 bytes putInt(c1,F); // longitude [seconds of arc] 4 bytes putShort(d1,F); // height [metres above sea level] 2 bytes /* PUT RADII FROM CENTRAL REFERENCE POINT TO BOUNDARY. Total disk storage for this is 720 bytes. */ int i; for(i = 0; i < 360; i++) // of each of the 360 degrees of bearing putShort(r1[i],F); // put the 16-bit radius in centimetres /* PUT HEIGHT OF POINT ON BOUNDARY ABOVE OR BELOW CENTRAL REFERENCE POINT The relief of the landshare is expressed as height difference [in cm] be- tween its central reference point and the point on its boundary for each of the 360 degrees of bearing. Each height is stored as a 16-bit quantity in the land share disk record. Requires 720 bytes of storage. */ for(i = 0; i < 360; i++) // of each of the 360 degrees of bearing putShort(h1[i] + 32768,F); // half full-house bias to make it monopolar fclose(F); // close the landshares } /* GENERATE LANDSHARE TEST DATA */ void T1regen(int n) { b1 = -71707; // latitude [example used: my apartment in Belo Horizonte] c1 = -158334; // longitude d1 = 582; // height int i; for(i = 0; i < 360; i++) // create circular 1-hectare landshare r1[i] = 5641.8958355; // radius of a 1-hectare circle [in centimetres] int x = -90; for(i = 0; i < 180; i++) // Create boundary that starts 90 cm below h1[i] = x++; // the height of the central reference point while(i < 360) // rises to 90cm above it and then descends h1[i++] = x--; // back down to 90cm below it. T1putLand(n); // store this landshare's test data to disk } /* Formats the latitude or longitude of the current Geographic Feature, supplied as an integral number of seconds of arc, within a 10-character array ready for display by the T0updt() function. */ char *RadToDMS(int x, int flag) { // convert radians to DEG:MIN:SEC format static char C[11] = "000:00:00N"; // template for display string if(flag == 0) // If we're formatting a latitude if(x < 0) { C[9] = 'S'; x = -x; } else C[9] = 'N'; else // else we're formatting a longitude if(x < 0) { C[9] = 'W'; x = -x; } else C[9] = 'E'; int y = x / 60, // integral minutes z = x % 60; // remaining seconds C[7] = (char)(z / 10 + 48); // integral 10s of seconds C[8] = (char)(z % 10 + 48); // integral remaining seconds x = y / 60; // integral degrees z = y % 60; // remaining minutes C[4] = (char)(z / 10 + 48); // integral 10s of minutes C[5] = (char)(z % 10 + 48); // integral remaining minutes y = x / 10; // 10s of degrees C[2] = (char)(x % 10 + 48); // integral remaining degrees C[1] = (char)(y % 10 + 48); // integral remaining 10s of degrees if(flag == 0) // if a latitude, C[0] = ' '; // prefix it with a space else // else it's a longitude, so show the C[0] = (char)(y / 10 + 48); // integral remaining 100s of degrees return &C[0]; // address of the output string } /* DISPLAY THE ACTUAL GRAPH ITSELF ON THE GRAPH SCREEN AREA. Called from only one place in t7graph(). */ void T1showHgt(int n) { T1getLand(n); XSetForeground(XD,XG,BLACK); XFillRectangle(XD,XW,XG,16,40,480,350); showMsg(0); // ANNOTATIONS FOR GRAPH'S VERTICAL AND HORIZONTAL SCALES char *S[6] = {"+10","+05"," 00","-05","-10"}, *T[14] = {"000","030","060","090","120","150","180", "210","240","270","300","330","360"}; int H = 97, // horizontal datum of graph V = 185; // vertical datum of graph // DRAW THE DARK GRATICULE FOR THE GRAPH XSetForeground(XD,XG,DARK); int i, j = V-101; for(i = 0; i < 5; i++) { // draw the long horizontal graticule lines XDrawLine(XD,XW,XG,H,j,H+360,j); j += 50; } j = H; for(i = 0; i < 13; i++) { // draw the long vertical graticule lines XDrawLine(XD,XW,XG,j,V-100,j,V+99); j += 30; } // DISPLAY THE GRAPH'S VERTICAL SCALE XSetForeground(XD,XG,GREY); XDrawLine(XD,XW,XG,H-2,V-101,H-2,V+99); // draw long vertical scale line j = V-101; for(i = 0; i < 21; i++) { // draw short horizontal scale marks XDrawLine(XD,XW,XG,H-6,j,H-2,j); j += 10; } j = V-96; for(i = 0; i < 5; i++) { // annotate the vertical scale XDrawString(XD,XW,XG,H-25,j,S[i],3); j += 50; } // DISPLAY THE GRAPH'S HORIZONTAL SCALE XDrawLine(XD,XW,XG,H,V+101,H+359,V+101); // draw the long horizontal scale j = H; for(i = 0; i < 13; i++) { // draw the short vertical scale marks XDrawLine(XD,XW,XG,j,V+101,j,V+106); j += 30; } j = H-8; for(i = 0; i < 13; i++) { // annotate the horizontal scale XDrawString(XD,XW,XG,j,V+118,T[i],3); j += 30; } // DRAW THE AXIS LABELS XDrawString(XD,XW,XG,H+96,V+134,"BEARING FROM NORTH [DEGREES]",28); XDrawString(XD,XW,XG,H-59,V-2,"HEIGHT",6); XDrawString(XD,XW,XG,H-59,V+10,"metres",6); XSetForeground(XD,XG,BLEEN); // set colour for the plot, then display it XDrawString(XD,XW,XG,H,V-120, "LANDSHARE'S BOUNDARY HEIGHT FOR EACH DEGREE OF BEARING",54); // DRAW THE GREEN BAR PLOTS INDICATING THE NUMBER OF NODES XSetForeground(XD,XG,GREEN); // set colour for the plot, then display it for(i = 0; i < 360; i++) { // for each of the 360 degrees of bearing int n = h1[i] / 10; // get the height on this bearing XDrawLine(XD,XW,XG,H+i,V,H+i,V-n); } } /* DISPLAY THE PLAN VIEW OF A LANDSHARE */ void T1showPlan(int n) { T1getLand(n); XSetForeground(XD,XG,BLACK); XFillRectangle(XD,XW,XG,16,40,480,350); showMsg(0); int i, X=350,Y=200; double a=0, b, r, x, y; // DISPLAY THE PLAN GRATICULE XSetForeground(XD,XG,DARK); y=-100; for(i = 0; i < 11; i++) { XDrawLine(XD,XW,XG,X-100,Y+y,X+100,Y+y); XDrawLine(XD,XW,XG,X+y,Y+100,X+y,Y-100); y+=20; } // DISPLAY THE LANDSHARE BOUNDARY XSetForeground(XD,XG,GREEN); for(i = 0; i < 360; i++) { r = r1[i]; // radius on current bearing in metres a += r*r*RPD/2; // add the area of each 1-degree sector r /= 50; // rescale radius to half-metres b = i * RPD; // convert the bearing from degrees to radians x = r * sin(b); // compute horizontal offset of boundary point y = r * cos(b); // compute vertical offset of boundary point XDrawPoint(XD,XW,XG,x+X,y+Y); } // DISPLAY THE SCALE AND COMPASS LINES XSetForeground(XD,XG,GREY); XDrawLine(XD,XW,XG,X-100,Y-150,X+100,Y-150); XDrawString(XD,XW,XG,X-108,Y-145,"W",1); XDrawString(XD,XW,XG,X+103,Y-145,"E",1); XDrawString(XD,XW,XG,X-30,Y-136,"100 metres",10); XDrawLine(XD,XW,XG,X-150,Y-100,X-150,Y+100); XDrawString(XD,XW,XG,X-152,Y-103,"N",1); XDrawString(XD,XW,XG,X-152,Y+113,"S",1); // DISPLAY THE DATA FIELD ANNOTATIONS Y=70; y=20; XDrawString(XD,XW,XG,17,Y,"REFERENCE: of ",21); XDrawString(XD,XW,XG,17,Y+=y,"LANDSHARE:",10); XDrawString(XD,XW,XG,17,Y+=y,"LATITUDE:",9); XDrawString(XD,XW,XG,17,Y+=y,"LONGITUDE:",10); XDrawString(XD,XW,XG,17,Y+=y,"HEIGHT: metres",21); XDrawString(XD,XW,XG,17,Y+=y,"AREA: sq metres",21); XDrawString(XD,XW,XG,17,Y+=y,"SOIL:",5); XDrawString(XD,XW,XG,17,Y+=y,"CLIMATE:",8); // DISPLAY THE DATA ITSELF Y=70; XSetForeground(XD,XG,BLEEN); XDrawString(XD,XW,XG,83,Y,showInt(n,3,1),3); XDrawString(XD,XW,XG,125,Y,showInt(n,3,1),3); XDrawString(XD,XW,XG,83,Y+=y,"D9A55BBAFC",10); XDrawString(XD,XW,XG,83,Y+=y,RadToDMS(b1,0),10); XDrawString(XD,XW,XG,83,Y+=y,RadToDMS(c1,1),10); XDrawString(XD,XW,XG,65,Y+=y,showInt(d1,6,0),6); XDrawString(XD,XW,XG,53,Y+=y,showInt((int)round(a/10000),5,0),5); XDrawString(XD,XW,XG,53,Y+=y,"Latosols red & yellow",21); XDrawString(XD,XW,XG,71,Y+=y,"Tropical wet & dry",18); } /* Soil Types: oxisols, aridsols, mollisols, alfisols, ultisols, spodsols, entisols, inceptisols, vertisols, histosols, and andisols. */ /* DRAW THE HORIZONTAL ROW OF CONTROL BUTTONS ACROSS BOTTOM OF LAND TAB Called from 1 place each in T1show(), T1click() and T1horizButClick(). */ void T1Buts() { if(tb != 1) return; // bail out if SCAN tab not currently visible showButs(); // show 8 blank buttons across the bottom of the tab area const char // annotate the horizontal row of control buttons *A[] = {"PLAN","VERT","SPARE","SPARE","SPARE","SPARE","REGEN","HELP"}; const int a[] = {15,15,12,12,12,12,12,15}, // horizontal offsets for word starts b[] = { 4, 4, 5, 5, 5, 5, 5, 4}; // number of letters in each word int h = 17; // x-coord of left side of first button for(int i = 0; i < 8; i++) { // for each of the 8 buttons: if(a1 & 1 << i) // if the button is 'on' XSetForeground(XD,XG,WHITE); // display it's lettering in white else XSetForeground(XD,XG,GREY); // default colour for the lettering XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]); h += 59; // move across to the next button } } /* MOUSE CLICKED ON A NON-BUTTON PART OF THE SCAN TAB. Called only from 1 place in MEH(). */ void T1click() {} /* ONE OF THE LAND TAB'S CONTROL BUTTONS WAS PRESSED Called from 1 place only in MEH()] */ void T1horizButClick(int x) { showMsg(0); // clear any pending message switch(x) { // switch according to button number case 0: // PLAN button clicked if(a1 & 1) // if it's on a1 &= ~1; // switch it off else { // if it's off T1showPlan(1); // show the landshare plan a1 &= ~0xFF; // switch off all buttons a1 |= 1; // and illuminate its button } break; case 1: // VERT button clicked if(a1 & 2) // if it's on a1 &= ~2; // switch it off else { // if it's off T1showHgt(1); // show the landshare's height profile a1 &= ~0xFF; // switch off all buttons 0xFF = 11111111 a1 |= 2; // and illuminate its button } break; case 6: // REGEN button clicked if(a1 & 0x40) // if the REGEN button is bright 0x40 = 01000000 a1 &= ~0x40; // switch it off [dim it] ~0x40 = 10111111 else { // else, the REGEN button is dim T1regen(1); // regenerate the land data showMsg(15); // show the 'regenerated' message a1 |= 0x40; // brighten the REGEN button } break; case 7: // HELP button was clicked helpPage("software/fep.html#land"); } // end of 'switch' T1Buts(); // re-display the buttons } /* DISPLAY THE STATIC CONTENT OF THE LAND TAB. Called only from 1 place in showTab(). */ void T1show() { int i, x; if(a1 == 0) a1 = 1; // if no button is bright, set button 1 bright for(i = 0; i < 8; i++) { // for each of the 8 buttons x = 1 << i; // make mask for the i-th bit if(a1 & x) { // if the i-th button is bright a1 &= ~x; // set it dim T1horizButClick(i); // then virtually click on it break; // and break out of the for() loop } } } /* FUNCTIONS PERTAINING TO TAB 0: NAME AND ADDRESS --------------------------- Need to devise a selector code to classify people as: members of one's community, members of one's coterie or simply an Ausländer. */ /* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE CROPS SUB-TAB. Called from T5crops(), T5cropList(), T5cropButClick(). */ void T0showDiaryButs() { // below are the names of the button annotations char *A[] = {"NEW","ED-PREV","ED-DONE","DONE","TOP","BOT","KILL","HELP"}; int a[] = {18, 6, 6,15,18,18,15,15}, // horizontal offsets for word starts b[] = { 3, 7, 7, 4, 3, 3, 4, 4}, // number of letters in each word h = 99; // y coordinate of button block for(int i = 0; i < 8; i++) { // for each of the 8 buttons: XSetForeground(XD,XG,DARK); // shade for buttons' background XFillRectangle(XD,XW,XG,17,h-15,53,21); if(v0 & 1 << i) // if this button is bright [on] XSetForeground(XD,XG,WHITE); // set colour for bright lettering else // otherwise XSetForeground(XD,XG,GREY); // set colour for dull lettering XDrawString(XD,XW,XG,17+a[i],h,A[i],b[i]); h += 27; // move across to the next button } } /* DRAW THE HORIZONTAL ROW OF CONTROL BUTTONS ACROSS THE BOTTOM OF THE TAB 0. Called once each from T0showNAD(), T0click(), T0horizButClick(), T0CR(). */ void T0Buts() { showButs(); // show 8 blank buttons across the bottom of the tab area const char // annotate horizontal row of control buttons *A[] = {"NEW","KILL","NEXT","PREV","LIST","DIARY","NOTES","HELP"}; const int a[] = {18,15,15,15,14,12,12,15}, // horizontal offsets for word starts b[] = { 3, 4, 4, 4, 4, 5, 5, 4}; // number of letters in each word int h = 17, c; // left edge of left-most button, colour for(int i = 0; i < 8; i++) { // for each of the 8 buttons: /* if one of the 4 indexes is active | AND we are dealing with the NEXT button | | | OR we're dealing with the PREV button | | | | | colour the button lettering bluey green | | | | | | */ if(b0 > 0 && (i == 2 || i == 3)) c = GREEN; else if(a0 & 1 << i) // if button 'i' is 'on' [bright] c = WHITE; // colour its lettering white else if(i == 5 && V8) // if it's the DIARY button and alert flag is set c = RED; // colour its lettering red else // otherwise the button is simply off c = GREY; // colour its lettering grey XSetForeground(XD,XG,c); // set the colour for the button lettering XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]); // draw the lettering h += 59; // move across to the next button } } /* DIARY SCROLL: Displays 11 lines of the diary in order of the highlighted column heading. Called by: FORMAT OF A DIARY RECORD: 4-byte integer:PREV date of contact | 4-byte integer:NEXT date of proposed contact | | 4-byte integer:DAYS to go to NEXT proposed contact | | | 4-byte integer: 'nad.dat' REF of person to whom | | | | the diary entry pertains 1111000011110000 */ void T0diaryScroll() { V8 = 0; // clear the 'diary alert' flag D0 = 2; // set scrolling dispatcher flag XSetForeground(XD,XG,BLACK); // clear the scrolling list area XFillRectangle(XD,XW,XG,87,84,397,210); e0 = Scroller(e0,N8); /* update mouse bias position & display scroll bar The record number, within 'diary.dat' as a whole, at which the currently selected index 'x0' starts as follows: x0 q | 0 000 | index number = column number 1 256 | | times 256 2 512 | | | 3 768 */ int q = x0 << 8, /* 4 1024 selected line within whole DIARY | mouse scroll bias for DIARY | | */ w = u0 - e0, // screen line number of currently selected item v = 100, i, // print-base of first data line, loop variable D = ratSysDate(); /* current system date in rationalised form LIST THE CURRENT SCREEN-FULL OF ITEMS */ for(i = 1; i < 12; i++) { /* for each of the 12 lines in the list record number within diary INDEX 'x0' of first displayed line | number of the current data line being displayed | | */ int c = e0 + i, // index record number within current diary INDEX r = q + c; /* index record number within 'diary.dat' as a whole [for non-indexed situation r = c automatically] */ int R = r; // first assume listing is in order of DIARY entry if(x0 > 0) { /* but if one of the indexes 1,2,3,4 is operative index file handle for 'diary.dat' | index record number within 'diary.dat' as a whole | | multiplied by 16 bytes per record | | | */ fseek(FB,r << 4,SEEK_SET); // seek start byte or INDEX record 'r' R = fgetc(FB); // get the DIARY record number from the INDEX record } fseek(FB,R << 4, SEEK_SET); // start byte of relevant DIARY record int p = getInt(FB), // PREV date n = getInt(FB), // DONE date d = getInt(FB), // DAYS to go a = getInt(FB); // NAD REF number [nad.dat record number] /* index record number within current index block | current 'nad.dat' record number of highlighted line | | 'nad.dat' record number from index file | | | */ if(c == u0) r0 = a; // 'nad.dat' record number of highlighted line // set the display colour: normal or highlighted int C = BLEEN, Q; if(i == w) C = WHITE; XSetForeground(XD,XG,C); XDrawString(XD,XW,XG,90,v,showInt(R,3,1),3); // display REF char *s; // show today's date in YELLOW instead of either BLEEN or WHITE Q = C; if(p == D) Q = YELLOW; XSetForeground(XD,XG,Q); if(p > 0) s = showInt(p,8,0); else s = "--------"; XDrawString(XD,XW,XG,116,v,s,8); // display PREV date // show today's date in YELLOW instead of either BLEEN or WHITE Q = C; if(n == D) Q = YELLOW; XSetForeground(XD,XG,Q); if(n > 0) s = showInt(n,8,0); else s = "--------"; XDrawString(XD,XW,XG,169,v,s,8); // display DONE date Q = BLEEN; // DISPLAY THE DAYS FIGURE if(n == 0) { // if this entry has no DONE date if(d < 0) // then if we still have some days left to go Q = GREEN; // show the number of days in green else { // else, we're late or need to attend to it today Q = RED; // so display the number of days in red V8 = 1; // and set 'diary alert' flag } s = showInt(d,4,0); // days figure to string } else s = "----"; // DONE date exists so DAYS figure is irrelevant XSetForeground(XD,XG,Q); XDrawString(XD,XW,XG,225,v,s,4); // FINALLY, GET AND DISPLAY THE PERSON'S FULL NAME XSetForeground(XD,XG,C); char S[36]; // to contain the person's name fseek(FA,a << 10,SEEK_SET); // start byte of person's name int l = fgetc(FA); // get the first byte of current record if(l < 0) l = 0; // invalid line length if(l > 36) l = 36; // upper safety limit for(int i = 0; i < l; i++) // for each character of the product name S[i] = fgetc(FA); // get it from the disk record XDrawString(XD,XW,XG,257,v,S,l); // display crop NAME v += 19; // drop down to the next line in the list } fseek(FB,2,SEEK_SET); // seek start byte or INDEX record 'r' fputc(V8,FB); // store the diary alert flag in byte 1 of 'diary.dat' } /* DIARY: Enables you to note the 'previous contact date', the 'proposed next contact date' and the reference number of the PERSON concerned. Instead of entering the 'next contact date', you can enter a number of days into the future when you should re-contact the person and the corresponding date is then calculated. Called by T0diaryDoneKill(), T0diaryClick(), T0LIST(), T0DIAR(), T0Esc(), T0show(). */ void T0diary() { clearTab(); // clear the whole tab int v = 70; // vertical coord of the lettering base of the title line // DISPLAY THE COLUMN HEADING BUTTONS XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,v,"DIARY:",6); XSetForeground(XD,XG,DARK); // colour for the title buttons XFillRectangle(XD,XW,XG, 87,55, 24,21); // REF button XFillRectangle(XD,XW,XG,116,55, 48,21); // PREV button XFillRectangle(XD,XW,XG,169,55, 48,21); // NEXT button XFillRectangle(XD,XW,XG,222,55, 30,21); // DAYS button XFillRectangle(XD,XW,XG,257,55,226,21); // NAME button // DISPLAY ALL COLUMN HEADINGS AND HIGHLIGHT THE SELECTED ONE if(m0 == 0) // if none of the column headings is selected m0 |= 8; // select the default column heading: 'REF' char *G[] = {"REF","PREV","DONE","DAYS","FULL NAME"}; int H[] = {91,128,181,225,340}, // horiz pixel start points of headings L[] = {3,4,4,4,9}; // number of letters in each heading for(int i = 0; i < 5; i++) { // for each of the 6 heading buttons int c = GREY; // colour for dim button [default] if(m0 & 1 << i) c = WHITE; // colour for highlighted button XSetForeground(XD,XG,c); // colour for the lettering XDrawString(XD,XW,XG,H[i],v,G[i],L[i]); // show heading lettering } XSetForeground(XD,XG,GLEEN); // colour for the data list lettering XDrawString(XD,XW,XG,17,318, "Click a column heading to list the items " "in the casting order of that column.",77); T0diaryScroll(); // display the scrollable list of diary lines T0showDiaryButs(); T0Buts(); } // DISPLAY NAME & ADDRESS FIELD NUMBER 'i'. Called by T0edit() and T0Esc(). void T0showField(int i) { en = fgetc(FA); // get content length from first byte of field if(en > L0[i]) // cap the content length of the line editor field en = L0[i]; for(i = 0; i < en; i++) // store the content into the line editor array le[i] = fgetc(FA); while(i < 47) // clear the remainder of the line editor array le[i++] = 0; XDrawString(XD,XW,XG,198,ey,le,en); } /* GET THE INNER BOX WIDTH FOR FIELD 'i'. Called by T0clearInsides(), T0clearInside() and T0showLE(). */ int T0BIW(int i) { int d = P0[i] - 1; if(i == 8 && a0 & 0x400) d = 20; // for Ref number entry return d; } /* CLEARS ANY VISIBLE EDITING BOX AND CREATES A BACKGROUND FOR EACH FIELD THE SIZE OF THE INSIDE OF A FIELD BOX. Called from T0showNAD(), T0edit() and T0srch(). */ void T0clearBoxes() { int d, // horizontal extent of the inside of the box v = 57; // y-coordinate of top of box area XSetForeground(XD,XG,BLACK); // colour to clear all boxes for(int i = 0; i < 13; i++) { // for each field of data XDrawRectangle(XD,XW,XG,194,v,P0[i],18); // clear data box v += 20; // drop down to next field } XDrawRectangle(XD,XW,XG,375,217,24,18); // clear Ref Number box } // CLEAR THE INSIDE OF ALL FIELD BOXES. Called by T0showNAD(), T0srch(). void T0clearInsides() { XSetForeground(XD,XG,DARK); // colour for the display field background int d, v = 58; for(int i = 0; i < 13; i++) { XFillRectangle(XD,XW,XG,195,v,T0BIW(i),17); // paint background v += 20; } XFillRectangle(XD,XW,XG,376,218,23,17); // clear record number XFillRectangle(XD,XW,XG,418,218,23,17); // clear occupied records XFillRectangle(XD,XW,XG,448,218,23,17); // clear max record number } void T0showIdx() { // Called by T0showNAD(), T0Show(), T0click(). int h = 21, v = 210; char *S[] = {"POSTAL CODE","FAST SEARCH NAME","PERSONAL IDENTIFIER", "COUNTRY CODE [DOMAIN]"}; int n[] = {11,16,19,21}, c; for(int i = 0; i < 4; i++) { if(b0 & 1 << i) c = GREEN; else c = GREY; XSetForeground(XD,XG,c); // shade for field name XDrawString(XD,XW,XG,h,v+=20,S[i],n[i]); } } void T0suppBut() { int c = GREY; if(D0 == 3) c = WHITE; XSetForeground(XD,XG,DARK); // set the button background colour XFillRectangle(XD,XW,XG,425,238,58,77); XSetForeground(XD,XG,c); // set the button background colour int v = 265, w = 16; XDrawString(XD,XW,XG,433,v,"FURTHER",7); v += w; XDrawString(XD,XW,XG,433,v,"INFORM-",7); v += w; XDrawString(XD,XW,XG,433,v," ATION ",7); } void T0showSupp() {} /* DISPLAY THE FULL NAME & ADDRESS INFORMATION IN RECORD NUMBER 'r0'. Called from: T0Show(), T0trawl() twice, T0selKey(), T0regen(), T0KILL(), T0nextprev(), T0NEXT(), T0PREV(), T0search(), T0CR(). */ void T0showNAD() { D0 = 0; // set scrolling dispatcher flag T0clearBoxes(); // clear any box to black T0clearInsides(); /* clear insides of boxes to grey global variable containing start byte of current name & address record | number of the record within 'nad.dat' that must be displayed | | shift left by 10 bits to multiply by 1024 bytes per record | | | */ q0 = r0 << 10; // to get the start byte within file of record number 'r' int j, // offset of current field within the current record d; // internal horizontal extent of current field ey = 70; // vertical position of the first display line XSetForeground(XD,XG,BLEEN); // colour for the display lettering for(int i = 0; i < 13; i++) { // FOR EACH OF THE 13 FIELDS OF DATA j = O0[i]; // offset of current field of data /* file handle for 'nad.dat' | start byte of current name & address record | | offset of current field of data | | | */ fseek(FA,q0 + j,SEEK_SET); // seek start byte of current data field // RETRIEVE AND DISPLAY THE FIELD CONTENT char S[48]; int K = fgetc(FA), k, // get content length from 1st byte of field L = L0[i]; if(K > L) K = L; // cap content length to field length for(k = 0; k < K; k++) // for each character of the field content S[k] = fgetc(FA); // copy it into the display array S[] S[k] = 0; // then terminate it with a NULL character XDrawString(XD,XW,XG,198,ey,S,K); // display the field ey += 20; // drop down one display line of 20 pixels } XDrawString(XD,XW,XG,379,230,showInt(r0,3,1),3); // record number XDrawString(XD,XW,XG,421,230,showInt(N0,3,1),3); // occupied records XDrawString(XD,XW,XG,451,230,showInt(H0,3,1),3); // highest occupied if(b0) T0showIdx(); // DISPLAY IN YELLOW ANY SEARCH TERM FOUND BY THE TRAWLER if(a0 & 0x100) { // if the trawler is active XSetForeground(XD,XG,DARK); /* set the field background colour index of the 1st char of the found search term within the field | number of horizontal pixels per character | | x-coordinate of the beginning of the field | | | */ int x = h0 * 6 + 198, // x-coordinate of start of found search term y = g0 * 20 + 70; /* y-coordinate of its lettering base line | | | | | y-coordinate of the lettering base line of the top field | inter-line drop between fields [in pixels] number [0 to 7] of the field containing the found search term height of the top of the field above the lettering base line | y-coord of the base line | length of the search term [in pixels] of the field lettering | | | horizontal pixels per character | | | | */ XFillRectangle(XD,XW,XG,x,y - 12,en * 6,17); // clear search term area XSetForeground(XD,XG,YELLOW); // colour for search term XDrawString(XD,XW,XG,x,y,le,en); // display the search term } T0suppBut(); T0Buts(); // display the control buttons for the name & address Tab } /* DISPLAY THE STATIC CONTENT OF THE NAME & ADDRESS TAB. See global variables section for Tab 0. Called only from 1 place in T0clearClick(), T0Esc(), showTab(). */ void T0Show() { clearTab(); int h = 21, v = 50; XSetForeground(XD,XG,GREY); // shade for field names XDrawString(XD,XW,XG,h,v+=20,"FULL NAME",9); XDrawString(XD,XW,XG,h,v+=20,"HOUSE/BUILDING",14); XDrawString(XD,XW,XG,h,v+=20,"STREET/ROAD",11); XDrawString(XD,XW,XG,h,v+=20,"DISTRICT/ZONE",13); XDrawString(XD,XW,XG,h,v+=20,"CITY/TOWN",9); XDrawString(XD,XW,XG,h,v+=20,"COUNTY/MUNICIPALITY",19); XDrawString(XD,XW,XG,h,v+=20,"STATE/ETHNIC COUNTRY",20); XDrawString(XD,XW,XG,h,v+=20,"SOVEREIGN STATE/COUNTRY",23); T0showIdx(); v = 290; XSetForeground(XD,XG,GREY); // shade for field names XDrawString(XD,XW,XG,h,v+=20,"SELECTOR KEY",12); XDrawString(XD,XW,XG,355,230,"REF of [ ]",20); // record numbers showMsg(0); T0showNAD(); // show first record } /* TEST TO SEE WHETHER OR NOT RECORD 'r' IS CURRENTLY OCCUPIED Called from T0trawl() and T0regen(). */ int T0isoc(int r) { /* file handle for main name & address file: 'nad.dat' | record number within 'nad.dat' | | DIVIDED by 8 ['record occupied' bits per byte] | | | */ fseek(FA,r >> 3,SEEK_SET); // seek byte containing the bit for record 'r' /* get the byte containing record 'r's 'occupied' bit | a '1' bit mask | | shifted left by an amount equal to | | | the remainder when 'r' is divided by 8 [bits per byte] | | | | */ if(fgetc(FA) & 1 << r % 8) // if the bit pertaining to record 'r' is set return 1; // return '1' [indicating that it's occupied] return 0; // else return '0' showing it is unoccupied } /* DISPLAY THE SCROLLABLE LIST OF PEOPLE Called from T0list(), MWup() & MWdn(). */ void T0listScroll() { D0 = 1; // set scrolling dispatcher flag XSetForeground(XD,XG,BLACK); // clear the scrolling list area XFillRectangle(XD,XW,XG,17,84,397,210); k0 = Scroller(k0,N0); // update mouse wheel position & display scroll bar int v = 100, // lettering base of first data line w = l0 - k0, // line number of currently selected person I[] = {3,0,1}, // index number from column number b = 0; // start byte of current index if(d0 < 3) /* if one of the indexed fields is selected: skip over the 262144 [0x40000] bytes of PEOPLE data records | index number 0,1,2 of current index | | column number of current sort field | | | times 4096 bytes per index | | | | */ b = 0x40000 + (I[d0] << 12); // start byte of current index for(int i = 1; i < 12; i++) { // for each of the 11 lines in the list int r = i + k0; // record number of current 1st line in list if(d0 < 3) { /* if listing in the order of one of the indexes 0, 1, 2 SEEK THE START BYTE OF RECORD 'r' WITHIN THE INDEX I[d0] IN 'nad.dat' index file handle for 'nad.dat' | start byte of current index | | record number within current index | | | multiplied by 16 bytes per record | | | | */ fseek(FA,b + (r << 4),SEEK_SET); r = fgetc(FA); /* 'nad.dat' record number from index file GET RECORD NUMBER IN 'nad.dat' OF THE CURRENTLY HIGHLIGHTED LINE number of the first currently-displayed line | list line number of first display line | | line number within complete total list in file | | | current 'nad.dat' record number of highlighted line | | | | 'nad.dat' record number from index file | | | | | */ if(i + k0 == l0) r0 = r; } else // else we are listing in plain REF number order, so r0 = l0; // the 'nad.dat' record number is simply the line number if(r > H0) break; // break if 'r' is beyond last occupied record if(!(T0isoc(r))) continue; // skip if this record is unoccupied int B = r << 10; // start byte of the current record char s[41]; int X[] = { 20, 40,111,233}, // x-coords for start of columns content Y[] = { 2, 10, 14, 41}, // maximum content length for each column Z[] = {485,384,409, 0}; // filed offsets in the 'nad.dat' record XSetForeground(XD,XG,BLEEN); // colour for normal line if(i == w) // if doing the highlighted line XSetForeground(XD,XG,WHITE); // set lettering colour to WHITE for(int k = 0; k < 4; k++) { // for each of the 5 columns fseek(FA,B+Z[k],SEEK_SET); // start byte of field in 'nad.dat' record int l = fgetc(FA), m; // length of the field text if(l < 0) l = 0; // guard against corrupted disk if(l > (m = Y[k])) l = m; // limit content length to column width for(int i = 0; i < l; i++) // for each byte of the column content s[i] = fgetc(FA); // put it sequentially in array s[] XDrawString(XD,XW,XG,X[k],v,s,l); // display the column content } XDrawString(XD,XW,XG,208,v,showInt(r,3,1),3); // show the REF number v += 19; // drop down to the next line in the list } } /* DISPLAY NAMES AND ADDRESSES AS A SCROLLABLE LIST Called from T0horizButClick(). */ void T0list() { clearTab(); // clear the whole tab showMsg(0); // kill the trawl message int v = 70; // vertical coordinate of the lettering base of the title line // DISPLAY THE COLUMN HEADING BUTTONS XSetForeground(XD,XG,DARK); // colour for the title buttons XFillRectangle(XD,XW,XG, 17,55, 18,21); // COUNTRY CODE button 2 chars XFillRectangle(XD,XW,XG, 40,55, 66,21); // POSTAL CODE button 10 chars XFillRectangle(XD,XW,XG,111,55, 90,21); // SEARCH NAME button 14 chars XFillRectangle(XD,XW,XG,206,55, 22,21); // REF button 3 chars XFillRectangle(XD,XW,XG,233,55,250,21); // FULL NAME button 41 chars // DISPLAY ALL COLUMN HEADINGS AND HIGHLIGHT THE SELECTED ONE char *G[] = {"CC","POST CODE","SEARCH NAME","REF","FULL NAME"}; int H[] = {20,46,122,209,325}, // horiz pixel start points of headings L[] = {2,9,11,3,9}; // number of letters in each heading for(int i = 0; i < 5; i++) { // for each of the 6 heading buttons int c = GREY; // colour for dim button [default] if(c0 & 1 << i) c = WHITE; // colour for highlighted button XSetForeground(XD,XG,c); // colour for the lettering XDrawString(XD,XW,XG,H[i],v,G[i],L[i]); // show heading lettering } v = 100; // base line of the 1st data row's lettering XSetForeground(XD,XG,GLEEN); // colour for the data list lettering XDrawString(XD,XW,XG,30,316, "Click a column heading to list people " "in the casting order of that column.",74); T0listScroll(); // display the scrollable list of people data lines T0Buts(); // display the horizontal buttons of the PEOPLE tab } // CLEAR THE INSIDE OF A FIELD BOX. Called from T0srch() and T0Esc(). void T0clearInside() { XSetForeground(XD,XG,DARK); // colour for the display field background XFillRectangle(XD,XW,XG,195,ey-12,T0BIW(f0),17); // paint the background } /* BOX THE FIELD 'f0' IN THE GIVEN COLOUR. Called from T0edit(), T0srch(), T0Esc(). */ void T0drawBox(int c) { XSetForeground(XD,XG,c); /* set the specified colour for the box vertical top corner of box to be displayed | outer box width of field 'f0' | | */ XDrawRectangle(XD,XW,XG,194,ey-13,P0[f0],18); // draw the box } /* ONE OF THE NAME & ADDRESS DATA FIELDS WAS CLICKED. This function displays a yellow box round the clicked field and sets up the line editor for edit- ing the field's content. Called from only one place in T0click(). */ void T0edit(int f) { // f=number of screen field that's just been clicked if(ea == 0) { // if no field is currently being edited f0 = f; // field number of the clicked line T0clearBoxes(); /* clear all name & address fields start byte of field 'f0' in record 'r0' of name & address file 'nad.dat' | first byte of the record 'r0' of the name & address file 'nad.dat' | | off-set [in bytes] of the start byte of field 'f0' | | | number of the clicked field | | | | */ p0 = q0 + O0[f0]; fseek(FA,p0,SEEK_SET); // seek start byte within 'nad.dat' XSetForeground(XD,XG,WHITE); // colour for field being edited T0showField(f0); // get and display the clicked field in white T0drawBox(YELL); // box field 'f0' in the given colour ea = 1; // activate line editor } else { // else, if this field is already being edited if(f != f0) return; // exit if click was not in field being edited doCursor(0); // erase the cursor from its old position } ec = (mx - ex) / 6; // compute cursor's character position within field if(ec > en) // if click was beyond the end of the line content ec = en; // place cursor at end of current line content doCursor(1); // draw the cursor } /* A NAME & ADDRESS FIELD NAME WAS CLICKED. This function sets up the corr- responding data field with a green box ready for entering a search term. Called from only one place in T0click(). */ void T0srch(int f) { if(ea == 0) { // if no field is currently being edited f0 = f; // set clicked screen field as the one being edited T0clearBoxes(); // clear all name & address fields T0clearInsides(); // clear the insides of all field boxes T0drawBox(GREEN); // box field 'f0' in the given colour for(int i = 0; i < 48; i++) le[i] = 0; // and clear the line editor array en = 0; // set content of line editor array to zero ec = 0; // set cursor to start of field ea = 1; // activate line editor doCursor(1); // draw the cursor } else { // else, if this field is already being edited if(f0 != f) // if click was not in field being edited return; // exit doCursor(0); // erase the cursor from its old position } T0clearInside(); // clear the inside of the editing box } /* SET UP A TRAWL SEARCH. This searches all the non-indexed fields of all the name & address records for the occurrence of the entered search term. Before comparing, it converts both the search term and the retrieved field content to upper case letters only. Called only from 1 place each in T0NEXT() and T0CR(). */ void T0trawl() { int B = r0 << 10; // start byte of record '1' [record number times 1024] a0 |= 0x100; // set the 'trawler active' bit showMsg(6); // show message "Trawling for search term." SUC(en,le); // convert search term to upper case only for(int i = r0; i <= H0; i++) { // for each record in 'nad.dat' if(T0isoc(i) == 0) continue; // skip this record if it is unoccupied int o = 0; // offset of first field is zero for(int j = 0; j < 8; j++) { // for each of the record's first 8 fields int K, // length of the content of the current field k; // individual character retrieved from disk record char S[48], // to hold the field content read from the disk record *s; /* return pointer from the string comparator strstr(); file handle for 'nad.dat' | start byte of current record | | offset of current field within record | | | */ fseek(FA,B + o,SEEK_SET); /* seek start byte of current field if the length of this field's content | as retrieved from disk | | is more than the permitted maximum of 47 bytes | | | truncate it to the permitted 47 bytes | | | | */ if((K = fgetc(FA)) > 47) K = 47; // get length of field content if(K < 0) K = 0; // zero it if negative for(k = 0; k < K; k++) // copy the field content S[k] = fgetc(FA); // into the array S[] S[K] = 0; // terminate the content with a NULL character SUC(K,S); /* convert it S[] content to upper case only points to character within field at which found search term begins | content of current field | | entered search term | | | if search term is not found in this field | | | | increment offset to start of next field | | | | | loop back to do next field | | | | | | */ if((s = strstr(S,le)) == NULL) { o += 48; continue; } /* char number within array [anything from zero to number of elements - 1] | is the pointer to the character of interest within the array | | minus the name of the array [the pointer to the first element] | | | */ h0 = s - S; // index of start of found search term within the field r0 = i; // number of record containing found search term g0 = j; // number of the field containing the found search term T0showNAD(); // display name & address with found search term showMsg(7); // display the "search term found" message in green return; // can return immediately without breaking the loops } B += 1024; // advance to the start byte of the next record } showMsg(8); // show message that the search term could not be found a0 &= ~0x100; // kill the trawl mode r0 = 1; // set back to first record T0showNAD(); // and display it } /* THE CLICK WAS IN A BLANK PART OF THE NAME & ADDRESS TAB. This function clears all active modes and re-displays the name & address screen. Called from 3 places in T0click(). */ void T0clearClick() { b0 = 0; // set to NEXT/PREV by Ref Number [not in any indexed order] a0 &= ~0xF00; /* clear the 'search mode', 'trawler mode', 'Ref No. entry mode' and 'selector key mode' bits */ T0Show; // re-display field names & button states } /* EDIT REF NUMBER FIELD TO SEARCH FOR A NAME & ADDRESS RECORD BY REF NUMBER Called only from one place in T0click(). */ void T0edRef() { ea = 1; // 1: editor active; 0: editor inactive ey = 230; // line editor's current vertical lettering base coordinate ex = 379; // line editor's current horizontal lettering coordinate en = 0; // current length of the line editor's field content ec = 0; // current cursor position [line editor] a0 |= 0x400; // indicates Ref Entry Mode f0 = 8; // screen field number XSetForeground(XD,XG,GREEN); // colour for search box XDrawRectangle(XD,XW,XG,375,217,24,18); // show data box } /* PRINT THE NAME & ADDRESS RECORD THAT MATCHES THE ENTERED SELECTOR CODE Called only once from T0selKey() below. */ void T0prtRec(int r, int B, FILE *F) { char S[48]; fprintf(F,"%s ",showInt(r,3,1)); // print the record number for(int i = 0; i < 13; i++) { // for each of the 9 NAD fields fseek(FA,B + O0[i],SEEK_SET); // get start byte of [next] field int j, J = fgetc(FA), // get the field's content length L = L0[i]; if(J > L) J = L; // cap content length to field length /* load the content of the field into the S[] array and terminate it with a NULL character */ for(j = 0; j < J; j++) S[j] = fgetc(FA); S[j] = 0; if(i == 12) // if this is the last field fprintf(F,"%s\n",S); // print it and terminate it with a CR else // for all other fields fprintf(F,"%s, ",S); // print and terminate with a comma + space } } /* EXAMINE THE PRESENTED CHARACTER 'c'. IF IT IS A LOWER CASE LETTER, MAKE IT UPPER CASE. IF IT IS NETHER A LETTER NOR A NUMBER, RETURN A NULL CHARACTER. Called from T0selKey() and T0key(). */ int AZ09(int c) { if(c >= 'a' && c <= 'z') c -= 32; // make 'c' an upper case only letter /* if the value in variable 'c' is lower in ASCII code order than the character '0' or it is between character '9' and character 'A' or it is between character 'Z' and character 'a' or it is beyond character 'z' then make it character '0' */ if(c < '0' || c > '9' && c < 'A' || c > 'Z' && c < 'a' || c > 'z') c = 0; return c; } /* LIST THE NAMES & ADDRESSES THAT MATCH THE ENTERED KEY SETTINGS. The best policy for security & legality is to print the list to a file 'list.txt'. Called from only one place in T0CR(). */ void T0selKey() { ea = 0; // disable the line editor FILE *F = fopen("list.txt","w"); // open the list file for writing fprintf(F,"# SELECTOR KEY LIST MATCHING THE ENTERED PATTERN %s\n\n",le); int i, g = 0, // 'print all records' flag B = 1504; // start byte of SELECTOR KEY field of record 1 if(le[0] == '*') // if just an asterisk entered as search term g = 1; // set to print all records else for(i = 0; i < en; i++) // limit the characters of the search le[i] = AZ09(le[i]); // pattern from 0 to 9 and from A to Z for(i = 1; i <= H0; i++) { // FOR EACH NAME & ADDRESS RECORD: fseek(FA,B,SEEK_SET); // get start byte of its SELECTOR CODE field int f = 1; // set flag to include current record if(g == 0) // if not ordered to print all records if(fgetc(FA) == 0) // if nothing's ever been entered into its selector f = 0; // code field, then don't consider this record else // ONLY SELECT RECORDS THAT MATCH THE ENTERED SELECTOR KEY for(int j = 0; j < en; j++) /* for each char of entered SELECTOR KEY if the character of the entered pattern | is different from | | the character from the disk record | | | set the flag to 'fail' | | | | */ if(le[j] != fgetc(FA)) { f = 0; break; } /* | then break out of the character test loop because there's no point in testing any further characters */ if(f == 1) /* if the match condition survived number of the current record | start byte of current record's SELECTOR CODE field | | to get start byte of the current record | | | file handle for the output text file 'list.txt' | | | | */ T0prtRec(i,B - 480,F); // print the name & address record B += 1024; // advance to start byte of next record's SELECTOR KEY field } fclose(F); // close 'list.txt' a0 &= ~0x800; // kill SELECTOR KEY search mode T0showNAD(); // re-display the original name & address system("gedit list.txt &"); // display the list in text editor } /* SAVE TO DISK THE TOTAL NUMBER OF CURRENTLY OCCUPIED RECORDS 'N0' AND THE NUMBER OF THE HIGHEST OCCUPIED RECORD 'H0'. Called from T0regen(), T0occu(), T0newIdx(). */ void T0putN0H0() { fseek(FA,32,SEEK_SET); // seek byte 32 of record zero of 'nad.dat' fputc(N0,FA); // store the number of occupied records fputc(H0,FA); // store number of highest occupied record fflush(FA); // store it to disk immediately } /* GENERATE OR RE-GENERATE THE DIARY INDEXES. All indexes have 16-byte rec- ords: 1 byte for record number and 4 bytes each for the PREV date, NEXT date and DAYS to go columns. There are 4 indexes: REF order, PREV order, NEXT order and DAYS to go order. They are stored one after the other in 'diary.dat' [handle=FB] and contain 4096 bytes each, total: 20480 bytes. Called once each in T0diaryPostEdit(), T0diaryDoneKill(), T0diaryClick(). The file 'diary.dat' contains the following: 256 * 16-byte records for the diary itself 256 * 16-byte records for the PREV dates index 256 * 16-byte records for the NEXT dates index 256 * 16-byte records for the DAYS to go index 256 * 16-byte records for the NAD REF index --- 1280 * 16 = 20480 bytes total Record Zero of 'dairy.dat' contains: Bype 0: N8 current number of items in the diary Byte 1: V8 current setting of 'diary alert' flag Byte 2: W8 the 'sort required' bits for the 4 indexed fields Format of a DIARY record: the DIARY's data fields: 4-byte integer:PREV date of contact | 4-byte integer:NEXT date of proposed contact | | 4-byte integer:DAYS to go to NEXT proposed contact | | | 4-byte integer:'nad.dat' REF of person to whom | | | | the diary entry pertains 1111000011110000 Format of a diary INDEX record for each of the 4 indexes respectively: 1-byte diary ref number |1-byte string length [always 8] ||8-byte string version of integer for PREV,NEXT, DAYS or NAD REF field ||| 0111111111000000 remainder of 16-byte record is filled with nulls */ void T0diarySort(int i) { // 'i' = number 1 to 3 of the index to be sorted if(i == 0) return; /* we never need to sort the REF index so exit indicates that the DAYS to go index is already sorted |indicates that the DONE dates index is already sorted ||indicates that the PREV dates index is already sorted |||bit corresponding to the REF number index [not used] |||| 00001110 Q = Byte 1 of 'diary.dat' | | if index i's sorted bit is set [index 'i' doesn't need sorting] | | */ if(W8 & 1 << i) return; // then exit without doing anything int P[] = {0,0,4,8,12}, // 10000111100001 field offsets p = P[i], // start byte of INDEX i's data field within t, // Record Zero of the DIARY, elapsed time [DAYS] d = ratSysDate(), /* current system date in rationalised form start byte of Record Zero of the 'i-th' INDEX within 'diary.dat' | index number 1,2,3,4 | | multiply by 4096 bytes per index | | | */ q = i << 12, j, J = N8 + 1, k; // loop variables char *s; // to hold a record's content showMsg(20); /* show the 're-sorting' message COPY ALL RECORDS OF THE 'REF' INDEX INTO THE 'i-th' INDEX current DIARY record number | */ for(j = 1; j < J; j++) { // for each of the diary records from 1 to 255 p += 16; // advance to [first/next] DIARY record if(i == 3) { // if doing the DAYS index fseek(FB,p-8,SEEK_SET); // start byte of INDEX i's PREV field in DIARY int a = getInt(FB), // get the PREV date as an integer b = getInt(FB); // get the NEXT date as an integer if(b == 0) // If the NEXT date is zero find the number t = getET(a,d); // of days from the PREV date to today else // else t = 9999; // set the DAYS value to maximum fseek(FB,p,SEEK_SET); // start byte of INDEX i's data field in DIARY putInt(t,FB); // store the days to go figure s = showInt(t,8,1); // form 't' into a 8-character string } else { // else, for indexes 1 ans 2 fseek(FB,p,SEEK_SET); // start byte of INDEX i's data field in DIARY s = showInt(getInt(FB),8,1); // set the DIARY field content as an 8 } // character string with leading zeros q += 16; // advance to [first/next] index record fseek(FB,q,SEEK_SET); // start byte of next record of index file fputc(j,FB); // store the DIARY record number fputc(8,FB); // store the string length for(k = 0; k < 8; k++) // put the record's field content into INDEX 'i' fputc(*(s+k),FB); while(k < 14) { // pad out the array to 14 characters fputc(0,FB); k++; // using NULL characters } } int w = i << 8; // record number in 'diary.dat' for Record 0 of index 'i' FS = FB; // index file handle for idxSort() idxSort(w+1,w+N8); // SORT INDEX 'i' fseek(FB,3,SEEK_SET); // seek Byte 2 of 'diary.dat' fputc(W8 & ~(1 << i),FB); // clear the 'sort required' bit for this index fflush(FB); // flush the sorted index to disk showMsg(14); // show 'index regeneration completed' message } /* GENERATE OR RE-GENERATE THE POST CODE, SEARCH NAME, PERSONAL IDENT AND COMMUNICATIONS ID INDEXES. All indexes have 16-byte records: 1 byte for record number, 1 byte for text content length and 14 bytes for text con- tent. All indexes are stored one after the other after the PEOPLE data in 'nad.dat'. Called from 1 place in T0vButClick(). */ void T0regen() { for(int i = 0; i < 4; i++) { // for each of the 4 index files int p = O0[i + 8], /* offset of indexable field in NAD record skip the 262144 [0x40000] bytes of the PEOPLE data in 'nad.dat' | current index number | | times 4096 [0x1000] bytes per index | | | */ q = 0x40000 + (i << 12), // start byte of current index within nad.idx j, k; // inner loop variables N0 = 0; // number of occupied records H0 = 0; // highest occupied record for(j = 1; j < 256; j++) { // for each of the 255 records from 1 to 255 p += 1024; // start byte of indexed field in [first/next] NAD record /* You only advance to the next index record when an OCCUPIED nad.dat record is encountered, otherwise you don't advance along the index. */ if(T0isoc(j)) { // if this [the j-th] record is occupied char S[16]; q += 16; // advance to [first/next] index record fseek(FA,p,SEEK_SET); // start byte of nad record's indexable field int l = fgetc(FA); // get length of this field [unsigned char] if(l > 14) l = 14; // limit index field text to 14 characters for(k = 0; k < l; k++) // get the first 'l' characters of the field S[k] = fgetc(FA); // and put them into the character array s[] fseek(FA,q,SEEK_SET); // start byte of next record of index file fputc(j,FA); // put 'nad.dat' record number to index file fputc(l,FA); // put length of field content to index record for(k = 0; k < l; k++) // put the first 'l' characters of the field fputc(S[k],FA); // into the corresponding index record while(k < 14) { // Fill the remainder of the 14 character fputc(0,FA); k++; } // field with null characters. N0++; // increment the number of occupied records H0 = j; // highest occupied record = current record } } /* idxSort works with 16-byte records. There are 64 * 16-bytes records in one 'nas.dat' data record. There are 256 data records, which is therefore equivalent to 256*64=16384 [0x4000] 16-bit records. pseudo 16-byte record number of start of the index to be sorted | number of equivalent 16-byte records in 'nad.dat' PEOPLE data | | index number 0 to 3 of the index being sorted | | | times 256 16-byte records per index | | | | */ int w = 0x4000 + (i << 8); // nad.dat rec num for rec 0 of current index FS = FA; // index file handle for idxSort() idxSort(w+1,w+N0); // sort this index file } fflush(FA); // flush the sorted indexes to disk T0putN0H0(); // save 'N0' and 'H0 and flush to disk r0 = 1; // display first record after an index regen showMsg(14); // show 'index regeneration completed' message } /* CREATES AN ENTRY FIELD FOR A NEW ENTRY OR FOR EDITING AN EXISTING PREV OR DONE DATE. Called from T0diaryClick(). */ void T0diaryPreEdit() { /* clear any old lingering message | if we're doing an ED-PREV or ED-DONE | | and a diary entry not selected [highlighted] | | | show 'no entry selected' message | | | | */ showMsg(0); if(v0 & 6 && u0 == 0) { showMsg(21); return; } T0diary(); int x = 0; // 'x' is the horizontal field offset if(v0 & 4) // which is zero for either a NEW or ED-PREV entry x = 53; // or 53 for an ED-DONE entry ex = 116 + x; // line editor's horizontal lettering coordinate ey = 316; // line editor's vertical lettering base coordinate XSetForeground(XD,XG,BLACK); // clear the original text XFillRectangle(XD,XW,XG,17,ey-14,466,18); XSetForeground(XD,XG,BLEEN); // colour for the data list lettering XDrawString(XD,XW,XG,173+x,ey,"Type-in the new date.",21); XSetForeground(XD,XG,GREEN); // colour for the box XDrawRectangle(XD,XW,XG,ex-4,ey-14,54,19); en = 0; // current length of the line editor's field content ec = 0; // current cursor position [line editor] doCursor(1); // display the cursor at start of field initially ea = 1; // 1: editor active; 0: editor inactive } int T0diarySeek(int y) { int r = u0; // set record number to the selected line number if(x0 > 0) { // but if displayed list is in PREV, DONE or DAYS order /* The record number of the highlighted line within the DIARY section of 'diary.dat' must be got from the first byte of the record within the cor- responding index section [1,2 or 3] of 'diary.dat' whose number is that of the highlighted line within the displayed list. file handle for 'diary.dat' | selected line in current INDEX: 1, 2 or 3 | | column heading [index] number 0 to 3 [default is 0: REF] | | | times 256 records per index | | | | all multiplied by 16 bytes per record | | | | | */ fseek(FB,(u0 + (x0 << 8)) << 4,SEEK_SET); r = fgetc(FB); // DIARY record containing the marked line's data } /* Now seek the start byte of either 1) the DIARY data record itself if deleting a diary entry [y = 0], or 2) that of the DONE field within the diary data record [y = 4]: file handle for 'diary.dat' | DIARY record containing the marked line's data | | multiplied by 16 bytes per record | | | offset to start of required field in DIARY record | | | | */ fseek(FB,(r << 4) + y,SEEK_SET); return r; } /* POST-ENTRY PROCESSING OF A DIARY ENTRY AFTER CREATING A NEW ENTRY OR EDIT- ING AN EXISTING DATE. Executed in response to the user hitting the CR key after keying in the new date. Called only from one plane in T0CR(). */ void T0diaryPostEdit() { int y = 0, i, j; // for start-byte of the data field, loop variables if(v0 & 1) { // IF WE'RE DOING A NEW DIARY ENTRY int B = 16; // set start byte of diary record '1' if(N8 > 254) { // if diary is FULL [it already contains 255 entries] char s[16]; // holds content of a 16-byte diary record /* Collapse the whole diary down one entry, overwriting the first one in order to make room for the NEW one at the end.*/ for(i = 1; i < 255; i++) { // for each of the 255 diary records fseek(FB,B+16,SEEK_SET); // seek start byte of the record 'i+1' for(j = 0; j < 16; j++) // get the content of the record above s[j] = fgetc(FB); fseek(FB,B,SEEK_SET); // seek start byte of record 'i' for(j = 0; j < 16; j++) // overwrite record 'i' with the fputc(s[j],FB); // content of record i+1' in the diary B += 16; // advance to the next record } // thus vacating record 255 for NEW entry } else { // else there's still spare space in diary fseek(FB,0,SEEK_SET); // start byte of record zero fputc(++N8,FB); // increment & save the total number of entries fputc(0,FB); // trigger a resort of all diary indexes fflush(FB); // push this data to the physical disk } B = N8 << 4; // start byte of NEW record fseek(FB,B+12,SEEK_SET); // start byte of its REF number field putInt(r0,FB); // store the person's REFerence number e0++; // re-display from with new extra line fseek(FB,B,SEEK_SET); // seek start byte of new record } else { // ELSE WE'RE EDITING A 'PREV' OR 'DONE' DATE if(v0 & 4) y = 4; // the DONE field is 4 bytes from start of record T0diarySeek(y); // seek start byte of the record or its DONE field } le[8] = 0; // terminate the Line Editor's string for atoi() putInt(atoi(le),FB); // store the newly entered date v0 &= ~7; // dim the TOP 3 buttons u0 = 0; // clear any highlighted data line v0 &= ~255; // kill vertical buttons e0 = 0; // go to top line of diary T0diarySort(x0); // sort on the selected column if necessary T0diary(); // re-display the diary listing } /* PUT TODAY'S DATE AS THE 'DONE' DATE ON THE CURRENTLY SELECTED LINE OR DELETE THE HIGHLIGHTED DIARY ENTRY. Called from T0diaryClick() when the DONE or KILL button is clicked. */ void T0diaryDoneKill() { showMsg(0); // clear any remaining message if(u0 == 0) { // if no diary line is selected [highlighted] showMsg(21); return; // show the appropriate message and exit } int y = 0, q = 0; // field offset for start of record if(v0 & 8) y = 4; // field offset for DONE date int x = T0diarySeek(y); // get record number and seek start byte if(v0 & 8) // if the DONE button was clicked putInt(ratSysDate(),FB); // store today's date in the DONE field else { /* else we're doing a KILL, so move all records above the deleted entry down one place thereby overwriting the deleted record. */ if(x < N8) { // provided we are not on th last line int i, j, B = x << 4; // start byte of the record to be deleted char s[16]; for(i = x; i < N8; i++) { // for each record above the deleted one fseek(FB,B+16,SEEK_SET); // start byte of the record above for(j = 0; j < 16; j++) // get the content of the record above s[j] = fgetc(FB); fseek(FB,B,SEEK_SET); // start byte of the current record for(j = 0; j < 16; j++) // overwrite the current record with the fputc(s[j],FB); // content of the next record in the diary B += 16; // advance to the next record } } fseek(FB,0,SEEK_SET); // seek the start byte of 'diary.dat' fputc(--N8,FB); // decrement the total number of diary records fputc(0,FB); // trigger a resort of all diary indexes fflush(FB); // push this data to the physical disk u0 = 0; // unmark the deleted diary entry e0 = 0; // display from the top of the list q = 1; // message display flag } v0 &= ~255; // dim the TOP 3 buttons u0 = 0; // clear any highlighted data line T0diarySort(x0); // sort on the selected column if necessary T0diary(); // display the diary tab if(q) showMsg(22); // diary entry deleted message } /* HANDLES A 'PgUp' OR 'PgDp' KEY STROKE. Eleven lines of the DIARY or people LIST is displayed on the screen. A 'PgDn' keystroke causes the list to adv- ance by only 10 lines, leaving what was the bottom line of the list as its new top line. This gives better user confidence and sense of continuity. */ void T0PgUpDn(int s) { // 's' has a value of +10 for PgDn and -10 for PgUp int x = -11; switch(a0) { // switch on button logic case 0x10: // if we're in the LIST sub-tab k0 += s; // increment/decrement mouse bias by 10 lines if(s < 0) { // if it was a 'Page-Up' key-stroke if(k0 < 0) // if we've overshot the beginning of diary list k0 = 0; // display from the beginning of the diary list } else { // if it was a 'Page-Down' key-stroke x += N0; // start of the final 11 lines of diary list if(k0 > x) // if we've overshot the end of the diary list k0 = x; // set to display the final 11 lines } T0listScroll(); // re-display the diary list break; case 0x20: // if we're in the DIARY sub-tab e0 += s; // increment/decrement mouse bias by 10 lines if(s < 0) { // if it was a 'Page-Up' key-stroke if(e0 < 0) // if we've overshot the beginning of diary list e0 = 0; // display from the beginning of the diary list } else { // if it was a 'Page-Down' key-stroke x += N8; // start of the final 11 lines of diary list if(e0 > x) // if we've overshot the end of the diary list e0 = x; // set to display the final 11 lines } T0diaryScroll(); // re-display the diary list } } /* HANDLES MOUSE CLICKS ON THE DIARY SUB-TAB Called from only one place in T0click(). */ void T0diaryClick() { int i = -1; // value denotes a click that was not on any of the buttons // HANDLES A CLICK ON A DIARY COLUMN HEADING if(my > 54 && my < 76) { // headings buttons height limits if(mx > 86 && mx < 111) i = 0; // REF heading clicked else if(mx > 115 && mx < 164) i = 1; // PREV date heading clicked else if(mx > 168 && mx < 217) i = 2; // NEXT date heading clicked else if(mx > 221 && mx < 252) i = 3; // DAYS heading clicked if(i >= 0) { // if click was on a heading button v0 &= ~255; // kill vertical buttons u0 = 0; // clear any highlighted data line m0 = 1 << i; // set the heading column highlighting x0 = i; // update x0 only if a column heading has been clicked T0diarySort(i); // sort on the selected column if necessary } } // HANDLES A CLICK ON ONE OF THE DIARY'S VERTICAL BUTTONS else if(mx > 16 && mx < 70) { // if in horiz limits of vertical buttons int y = 83, Y = 105; // top & bottom limits of top button for(i = 0; i < 8; i++) { // for each of the 8 vertical buttons if(my > y && my < Y) // if mouse click was in this button's area break; // break out with i = button number y += 27; Y += 27; // otherwise move down to the next button } if(i < 8) { // if click was one of the buttons v0 &= ~255; // dim all vertical buttons buttons v0 |= 1 << i; // code for clicked button 1,2,4,8,16,32,64,128 switch(i) { // SWITCH ON BUTTON NUMBER case 0: // NEW case 1: // ED-PREV case 2: T0diaryPreEdit(); break; // ED-DONE case 6: // KILL case 3: T0diaryDoneKill(); break; // DONE case 4: e0 = 0; T0diaryScroll(); break; // TOP case 5: e0 = N8 - 11; T0diaryScroll(); break; // BOT case 7: helpPage("software/fep.html#diary"); // HELP } return; } } /* ELSE THE CLICK COULD HAVE BEEN ON ONE OF THE 11 LIST ITEMS. So if indeed the click was on one of the list items, compute the record number 'lo', within the index file, of the highlighted item. */ else if((i = listClick()) > 0) { u0 = i + e0; v0 &= ~255; } T0diary(); // [re]display the DIARY sub-tab } /* HANDLES MOUSE CLICKS ON THE PEOPLE LIST SUB-TAB Called from only one place in T0click(). */ void T0listClick() { int i = -1; if(my > 54 && my < 76) { // headings buttons height limits if(mx > 16 && mx < 35) i = 0; // CC heading clicked else if(mx > 39 && mx < 106) i = 1; // POST CODE heading clicked else if(mx > 110 && mx < 201) i = 2; // SEARCH NAME heading clicked else if(mx > 205 && mx < 228) i = 3; // REF heading clicked if(i >= 0) { // if click was on a heading button l0 = 0; // clear any highlighted data line c0 &= ~0xFF; // switch off all the heading column highlighting switch(i) { case 0: if(!(c0 & 1)) { c0 |= 1; } break; // Communications Code case 1: if(!(c0 & 2)) { c0 |= 2; } break; // POST CODE case 2: if(!(c0 & 4)) { c0 |= 4; } break; // SEARCH NAME case 3: if(!(c0 & 8)) { c0 |= 8; } // REFerence } d0 = i; // only update d0 if a column heading has actually been clicked } } /* ELSE THE CLICK COULD HAVE BEEN ON ONE OF THE 11 LIST ITEMS. So if indeed the click was on one of the list items, compute the record number 'lo', within the index file, of the highlighted item. column heading number | index record number of highlighted line | | */ else if((i = listClick()) > 0) l0 = i + k0; /* | scroll bias [number of records before the 1st one currently displayed */ T0list(); // [re]display the list sub-tab } /* MOUSE CLICKED ON A NON-BUTTON PART OF THE TAB. The click is therefore presumed to be an intentional click within a name & address data field. Called from 1 place each in T0NEW(), T0edField() and the Mouse Event Handler MEH(). */ void T0click() { if(tb != 0) return; // exit if this is not the PEOPLE tab showMsg(0); // clear the message line if(mx > 436 && mx < 495 && my > 250 && my < 328) { if(D0 == 3) D0 = 0; else D0 = 3; T0suppBut(); T0showSupp(); return; } switch(D0) { // scrolling dispatcher switch 0=NAD, 1=LIST, 2=DIARY, 3=SUPP case 1: T0listClick(); return; // click inside LIST sub-tab case 2: T0diaryClick(); return; // click inside DIARY sub-tab } if(mx > 16 && mx < 190) // if click was on a field name a0 |= 0x200; // indicates a search else if(mx > 197 && mx < 480) // if click was in a data field a0 &= ~0x200; // indicates an edit else { // else the click was somewhere else T0clearClick(); // on the name & address tab, so clear the return; // currently active states and exit } int f; // number of the field that's just been clicked for(f = 0; f < 13; f++) { // for each of the 13 lines on the screen int z = 57 + 20 * f; // compute top [y-coordinate] of clickable area if(my > z && my < z+18) // if click was within the vertical limits break; // break out of loop with f=clicked line number } /* if the click was at the level of field number 8 | but it is within the horizontal limits of the REF box | | | set up for editing the REF number | | | | */ if(f == 8 && mx > 354 && mx < 397) { T0edRef(); return; } /* and exit if a field name was clicked | and it was field 1,2,3,4,5,6 or 7, which can't be searched | | | | */ if((a0 & 0x200 && f > 0 && f < 8) || /* OR the click was on the level of one of the short 24-character fields | and it was beyond the end of the field | | clear currently active states and exit | | | */ (f > 7 && mx > 338)) { T0clearClick(); return; } ex = 198; /* set x-coordinate of start of first letter of field specified screen field number [0 to 13] | times 20 because there are 20 pixels per line [inter line spacing] | | base height of lettering for the top field | | | */ ey = f * 20 + 70; /* base height of lettering for the specified field | global variable set for use elsewhere */ int q = b0; // preserve the index selection setting b0 = 0; // clear all index settings T0showIdx(); // display all index field names in GREY T0Buts(); // display NEXT/PREV button annotations in GREY if(f > 7 && f < 12) { // if one of the indexable fields was clicked int i, j = f - 8; // number of the index [0 to 3] for(i = 0; i < 4; i++) // for each of the 4 index numbers [0 to 3] if(j == i) { // if 'i' is the same as the clicked index int k = 1 << i; // create a bit mask for this index's bit if(!(q & k)) // if the i-th index wasn't already selected b0 = k; // set only the bit for the i-th index break; // break out of the loop } } if(f > 12) return; // exit if not a valid field if(a0 & 0x200) { // if 'search mode' is active if(f == 12) { // if click was on the SELECTOR KEY name a0 &= ~0x200; // kill 'search mode' a0 |= 0x800; // set 'selector mode' instead } else // otherwise a0 &= ~0x800; // kill 'selector mode' in case it was set last time T0srch(f); // set up an empty green search box for clicked field } else T0edit(f); // else set up yellow editing box for the clicked field } /* FIND A NEW [SPARE, UNOCCUPIED] NAME & ADDRESS RECORD. The first 256 bits in record zero of 'nad.dat' are used to indicate whether or not a given name & address record is occupied. For instance if the tenth bit is set, it means that record 10 of 'nad.txt' is occupied with a current valid name & address. If the tenth bit is not set, it means that record No. 10 of 'nad.dat' is unoccupied [spare] and thus may be used for a new name and address to be entered. This function finds the number of the first available spare record in which to place a new name & address. Called only once from T0NEW(). */ void T0unocc() { fseek(FA,0,SEEK_SET); // seek the start byte of 'nad.dat' int a, // to hold the current new byte retrieved from disk i, j, // outer and inner loop variables k, // for the moving mask bit f = 0; // 'spare found' flag for(i = 0; i < 32; i++) { /* for each of the 32 bytes get byte number 'i' from record '1' of 'nad.dat' | if the byte is full of '1' bits | | skip to the next byte | | | */ if((a = fgetc(FA)) == 255) continue; k = 1; // initialise the test bit for(j = 0; j < 8; j++) { // for each of the 8 bits of the current byte if(a & k) k <<= 1; // if the bit is set, shift to next higher bit else { f = 1; break; } // set 'spare found' flag and break j-loop } if(f) break; // if spare found, break i-loop } if(f) { // if a spare record has been found a |= k; // set the found unset bit fseek(FA,i,SEEK_SET); // re-seek byte number 'i' fputc(a,FA); // re-write the byte to disk r0 = (i << 3) + j; // compute number of found 'nad.dat' record q0 = r0 << 10; // start byte of this 'nad.dat' record if(H0 < r0) // if new record beyond current highest occu- H0 = r0; // pied record, make it the new highest record } else showMsg(5); // else show 'no spare records available' message } // FIND THE NUMBER OF THE NEXT OCCUPIED RECORD. Called only from T0NEXT(). int T0next() { int B = r0 >> 3, // byte containing the 'occupied' bit for record 'r0' b = r0 % 8, // the 'occupied' bit within this byte for record 'r0' f = 0; // 'found' flag return value [0=no record found] fseek(FA,B,SEEK_SET); // seek the byte int c = fgetc(FA); // and get its content for(int i = 0; i < 256; i++) { // for a maximum of 256 records if(b > 6) { /* if r0's 'occupied' bit is the highest in the current byte advance to the next byte | if that now puts us beyond the last byte | | reset back to byte zero | | | */ B += 1; if(B > 31) B = 0; b = 0; // set to the first bit of the next byte fseek(FA,B,SEEK_SET); // seek the next byte c = fgetc(FA); // and get its content if(B == 0) continue; // loop back to skip record zero } else b++; // else simply advance to the next bit in the same byte if(c & 1 << b) { // if the record is occupied r0 = (B << 3) + b; // compute the number of this next occupied record f = 1; // set the 'found' flag break; // and break out of the for() loop } } return f; // return the 'found' [1=found; 0=not found] } /* FIND THE NUMBER OF THE PREVIOUS OCCUPIED RECORD Called by T0PREV() and T0KILL(). */ int T0prev() { int B = r0 >> 3, // byte containing the 'occupied' bit for record 'r0' b = r0 % 8, // the 'occupied' bit within this byte for record 'r0' f = 0; // 'found' flag return value [0=no record found] fseek(FA,B,SEEK_SET); // seek the byte int c = fgetc(FA); // and get its content for(int i = 0; i < 256; i++) { // for a maximum of 256 records if(b < 1) { // if r0's 'occupied' bit is the lowest in the current byte /* back up to the previous byte | if that now puts us before the first byte | | reset back round to byte 31 | | | */ B -= 1; if(B < 0) B = 31; b = 7; // set to the last bit of the previous byte fseek(FA,B,SEEK_SET); // seek the byte c = fgetc(FA); // and get its content } else b--; // else simply back down to the previous bit in the same byte if(B == 0 && b == 0) continue; // loop back to skip record zero if(c & 1 << b) { // if the record is occupied r0 = (B << 3) + b; // compute the number of this next occupied record f = 1; // set the 'found' flag break; // and break out of the for() loop } } return f; // return the 'found' [1=found; 0=not found] } /* SET OR UNSET THE 'OCCUPIED' BIT, IN RECORD ZERO, PERTAINING TO THE CURRENT RECORD 'r0'. Each record has an 'occupied' bit. When set, the record is deemed to be currently occupied with a current name and address. When unset, the record is deemed to be free to be used for a new name & address. There is a maximum of 255 possible occupied records. Consequently, there are 255 bits, 1 for each record. These are stored in the first 32 bytes [bytes 0 to 31] of 'nad.dat'. Byte 32 is used to store the current number of occupied records. Byte 33 is used to store the highest occupied record. Called from only 1 place each in T0KILL(). */ void T0occu(int q) { // q=1 set to occupied; q=0 unset to unoccupied /* file handle for 'nad.dat' | number of record currently being [or having been] edited | | divided by 8 to get the number of the byte | | containing the occupied bit for record 'r0' | | | */ fseek(FA,r0 >> 3,SEEK_SET); // byte containing the required required 8 bits int x = fgetc(FA); // get the byte containing the 'occupied' bit /* put a bit in position zero of the byte | shift it left by the number of places [bits] equal to the | | remainder when the record number 'r0' is divided by 8 | | | */ int y = 1 << r0 % 8; // make bit mask for the appropriate 'occupied' bit if(q) // if marking the record as occupied x |= y; // OR in the '1' bit corresponding to the current record number else // else, if unmarking the record as unoccupied x &= ~y; /* AND the existing bit pattern with the inverse of the bit mask To restore the byte to the same position we need to back up the file pointer by 1 byte because fgetc() has already advanced it. | */ fseek(FA,-1L,SEEK_CUR); fputc(x,FA); // restore the modified bit pattern if(q) { // if we're marking the record as now occupied N0++; // increment the number of occupied records if(r0 > H0) // if the new record is higher than the existing highest H0++; // occupied record, increment the highest occupied record } else { // else we're marking the record as now unoccupied N0--; // decrement the number of occupied records if(r0 == H0) // if we've just killed what was the highest occupied record H0--; // decrement the highest occupied record number /* NOTE: it's possible that the record prior to the one just deleted may be unoccupied. However, it doesn't matter if unoccupied records exist below 'H0' because these will be skipped when a NEXT/PREV operation is invoked. It is best to regard 'H0' simply as the maximum extent to which the file has so far been occupied. It has no further significance. */ } T0putN0H0(); // save 'N0' and 'H0 and flush to disk } // CLEAR THE ENTIRE NAME & ADDRESS RECORD 'r0'. Called only from T0NEW(). void T0clearNAD() { fseek(FA,q0,SEEK_SET); // start byte of new record for(int i = 0; i < 1024; i++) // set each of the 1024 bytes fputc(0,FA); // in the new record to NULL } /* BINARY SLICE SEARCH LOOP. This while() loop rapidly homes in to within one element of the one required. Called from one place only in T0findNAD(). */ int T0slice() { /* 'I' is the number 0 to 3 of the index to be searched number of the index record at the middle of the index range: 1 to N0 incl. | current number of occupied records in the index | | divide by 2 to get the number of the record half-way up the index | | | */ i0 = N0 >> 1; // start with the search term mid-way up the index int j = i0, // jump size starts at half index size f = 0; // string comparison flag while((j >>= 1) > 0) { // While halved jump size still > 0: if((f = strComp(I0+i0)) < 0) // if element is lower than search term i0 += j; // jump half-way higher up the index else if(f > 0) // if retrieved element is higher than search term i0 -= j; // jump half-way lower down the index else // otherwise they are equal so an exact match has been found return 0; // return 'exact match found' } return 1; // return 'nearest higher or lower match' found } /* SINGLE STEP INCHING FUNCTION: called when only a nearest match could be found by the binary slice search function. Return values: 0 means that the inching function has found an exact match 1 means that an exact match was not found by the stepping function 2 means top entry in index is the nearest lower match 3 means first entry in index is the nearest higher match Called from only one place in T0findNAD(). */ int T0step() { // NOTE: the search term is in global array st[] int u = 0, // 1 = going-up d = 0, // 1 = going-down f = 0; // string comparison flag while(u == 0 || d == 0) { // while stepping hasn't reversed direction if((f = strComp(I0+i0)) < 0) { /* if element lower than search term if moving up overshoots the top of the index | last entry is nearest lower match | | return 'above top of index' | | | */ if(++i0 > N0) { i0 = N0; return 2; } u = 1; // if not, set that we have moved up a peg } else if(f > 0) { /* if retrieved element higher than search term if moving down undershoots bottom of index | first entry is nearest higher match | | return 'we've hit the bottom of the index' | | | */ if(--i0 < 1) { i0 = 1; return 3; } d = 1; // if not, set that we have moved down a peg } /* otherwise an exact must have been found, so | return 'exact match' | | */ else return 0; } return 1; // an exact match could not be not found } // the match is still the nearest higher or lower /* INCH BACK DOWNWARDS TO THE FIRST EXACT MATCH. If the binary slice searcher [perhaps with the help of the inching function] found an exact match, it may not necessarily be for FIRST exact match in the index. The index may contain many identical entries and the previous functions may not have landed on the first one. It is therefore necessary to inch down the index until the 1st exact match is found. Called from 1 place in T0findNAD(). */ void T0firstExact() { while(--i0 > 0) // while not yet tested first index entry if(strComp(I0+i0) < 0) // if next lower element is lower than search term break; // break out of the while() loop i0++; // back up to the lowest exact match } /* FIND THE FIRST HIGHER NEAREST MATCH. If only a higher or lower nearest match could finally be found, we have to find the match that is nearest and higher. So first go down the index until you hit an entry that is less than the search term, then back up to the the previous entry. Then go up the index until you reach an entry that's greater than the search term then back down to the previous entry. */ void T0FHNM() { while(--i0 > 0) // while not yet reached the bottom of the index if(strComp(I0+i0) < 0) // if next lower element is less than search term break; // break out of the while() loop i0++; // then back up to where you were while(++i0 < N0) // while not yet reached the top of the index if(strComp(I0+i0) > 0) // if next higher element is greater than search break; // term, break out of the while() loop i0--; // then back down to where you were } /* SEARCH FOR THE ENTERED SEARCH TERM IN THE APPROPRIATE INDEX IN THE FILE 'nad.dat'. Sets index number 'i0' of the 1st exact match or 1st higher nearest match. Global array st[] contains the search term. Called from 1 place each in T0newIdx(),T0colIdx(),T0search() and twice in T0idxShft(). */ int T0findNAD(int l) { // search term [length 'l'] is already in 'st[]' for(int i = l; i < 14; i++) st[i] = 0; // fill remainder of search-term with NULL chars int f = T0slice(); // binary slice search finds i0 if(f == 0) // If the SLICER found an exact match T0firstExact(); // make sure it's the first one, else else { // slicer only managed to find NEAREST higher or lower f = T0step(); /* match, so continue search using the single STEPPER. f = 0 means that the inching function has found an exact match f = 1 means that an exact match was not found by the stepper f = 2 means top entry in index is the nearest lower match f = 3 means first entry in index is the nearest higher match */ if(f == 0) { // if the STEPPER managed to find an EXACT match T0firstExact(); // make sure it's the first one } else if(f == 1) // else if still only nearest higher/lower match found T0FHNM(); // find the nearest HIGHER match } showMsg(f+1); // display the result message for the search if(i0 < 1 || i0 > N0) // safety net i0 = 0; // set number of found record in global variable 'i0' return f; // return the match condition } /* SHIFT AN INTERVENING BLOCK OF INDEX RECORDS to overwrite the old index entry and vacate the appropriate position in the index to insert the new one. Called twice from T0idxShft() and once from T0newIdx(). */ void T0idxRecShft(int d, int i) { char S[16]; // stack array for moving the record content int k, /* common loop variable START BYTE OF RECORD 'i' WITHIN THE WHOLE OF 'nad.dat' | number of equivalent 16-byte records in 'nad.dat' PEOPLE data | | number of the 1st index record of search range | | | record number within current index search range | | | | times 16 to get start byte of index record | | | | | */ B = 0x400 + I0 + i << 4; /* SEEK START BYTE OF THE 'i+1'th OR 'i-1'th INDEX RECORD'S TEXT FIELD | file handle for the index file currently being dealt with | | start byte of current record | | | +1 for shifting it down [or -1 for up] the index | | | | times 16 to get start byte of index record | | | | | */ fseek(FA,B + (d << 4),SEEK_SET); for(k = 0; k < 16; k++) // copy whole content of record 'i +or- 1' S[k] = fgetc(FA); // into S[] array fseek(FA,B,SEEK_SET); // seek start byte of record 'i' for(k = 0; k < 16; k++) // copy the whole content of S[] fputc(S[k],FA); // into record 'i' } /* CREATE DUMMY INDEX ENTRIES FOR A NEW NAME & ADDRESS RECORD. For each of the 4 indexes: 1) find next higher place in existing index where the new dummy index entry must be placed, 2) increment 'N0' [number of index records] to accommodate new dummy entry, 3) shift all existing records from [including] next higher entry up one place, 4) insert new entry in vacated position, 5) put dummy entry also in the appropriate field of the name & address record in 'nad.dat'. Called only from one place in T0NEW(). */ void T0newIdx() { int i,j,k,K; // loop variables char st[14], // to assemble dummy entry *S = showInt(r0,3,1); // current record number as 3-digit string st[0] = '#'; // indicates that what follows is a record number st[1] = *S; // set 1st digit of record number in the search string st[2] = *(S+1); // set 2nd digit of record number in the search string st[3] = *(S+2); // set 3rd digit of record number in the search string for(i = 4; j < 14; i++) st[i] = 0; // fill the remainder of st[] with null chars FS = FA; // make general file handle = file handle for 'nad.dat' for(i = 0; i < 4; i++) { // for each of the 4 indexes + NAD fields I0 = i << 4; // index record number of record zero of Index 'i' T0findNAD(4); // find 'i0' where the dummy index entry must be put N0++; /* increment 'number of occupied records'. The highest occupied record H0 has already been incremented if necessary by T0unocc() */ for(j = N0; j > i0; j--) // for each record of Index 'i' T0idxRecShft(-1,j); /* shift all index records up by one place SEEK START BYTE OF THE NEW INDEX RECORD | file handle for 'nad.idx', which contains all 4 indexes | | number of equivalent 16-byte records in 'nad.dat' PEOPLE data | | | 'nad.idx' record number of start of index 'i' | | | | rec num of the new index record within Index 'i' | | | | | multiply by 16 bytes per record | | | | | | */ fseek(FA,0x400 + I0 + i0 << 4,SEEK_SET); fputc(r0,FA); // store the number of new 'nad.dat' record fputc(4,FA); // store the length of the dummy text content for(k = 0; k < 14; k++) // store the dummy text content fputc(st[k],FA); fflush(FA); /* update the index file on disk SEEK START BYTE OF THE CURRENT FIELD WITHIN THE NEW 'nad.dat' DATA RECORD | file handle for 'nad.dat' [main names & addresses file] | | start byte of the new NAD record [computed in T0unocc()] | | | plus the offset of this field within the NAD record | | | | number of the index being processed [0 to 3] | | | | | indexed field '0' is field '8' of the NAD record | | | | | | */ fseek(FA,q0 + O0[i + 8],SEEK_SET); fputc(4,FA); // store length of dummy text content K = L0[i+8]; // length of this indexed field in NAD record for(k = 0; k < K; k++) // store the dummy text content fputc(st[k],FA); } T0putN0H0(); // save 'N0' and 'H0' and flush to disk } /* THE 'NEW' BUTTON WAS CLICKED. Create new blank record and put dummy entries in each of the 4 indexes ready for editing. Called once in T0horizButClick(). */ void T0NEW() { if(a0 & 0x30) return; // exit if the LIST or DIARY button is on if(a0 & 1) { // if the NEW button be bright a0 &= ~1; // dim it } else { T0unocc(); // find record number of first spare record in 'nad.dat' T0clearNAD(); // clear the new name & address record ready for in-fill T0newIdx(); // create dummy index entries for a new NAD record T0showNAD(); // display the new clear name and address record mx = 198; // mouse x-coordinate of top field my = 58; // mouse y-coordinate of top field ea = 0; // set line editor to inactive T0click(); // effectively 'click' on the top field a0 &= ~0xFE; // dim all other bottom buttons 11111110 a0 |= 1; // brighten the NEW button b0 = 0; // set to Ref Number order [not index order] } } /* TO REMOVE A DELETED RECORD FROM THE INDEXES. COLLAPSES EACH OF THE FOUR INDEXES OVER THIS 'nad.dat' RECORD'S INDEX ENTRY in 'nad.idx'. The index file 'nad.idx' has 16 bytes per record. The record ranges for each of the 4 indexes are: i INDEXED FIELD RECORD RANGES I0 i0 0 POSTAL CODE 0 to 255 0 1 to 255 1 SEARCH NAME 256 to 511 256 1 to 255 2 PERSONAL ID 512 to 767 512 1 to 255 3 COMMS IDENT 768 to 1024 768 1 to 255 Note: the record's index entry is 'collapsed over' BEFORE the record itself is deleted. So, while the index entry is being removed, the record's number in nad.dat is still in global variable 'r0'. Called only from T0KILL(). */ void T0colIdx() { int i,j,J,f; // loop variables FS = FA; // set 'nad.dat' file pointer for strComp() for(i = 0; i < 4; i++) { /* FOR EACH OF THE 4 INDEXES: 1) GET THE SEARCH TERM FOR FINDING THE INDEX RECORD TO BE COLLAPSED OVER file handle for 'nad.dat' | current 'nad.dat' record's start byte | | offset of current indexed field in a 'nad.dat' record | | | */ fseek(FA,q0 + O0[i+8],SEEK_SET); // start byte of required field J = fgetc(FA); // field content length [unsigned char] if(J > 14) J = 14; // cap to upper limit /* Get content [text] of the i-th indexable field of record r0 of nad.dat to use as a search term to locate the record number 'i0' in index 'i'. */ for(j = 0; j < J; j++) // for each character of the field content st[j] = fgetc(FA); // put it in the corresponding element of st[] while(j < 14) { // fill the remainder of the array st[] st[j] = 0; j++; } /* with NULL characters 2) FIND THE ENTRY WITHIN INDEX 'i' THAT CONTAINS THE SEARCH TERM number of the first record in the search range [record '0' of index 'i'] | index number 0 to 3 of the index being collapsed | | multiplied by 256 records per index | | | */ I0 = i << 8; // index record number of record zero of index 'i' f = T0findNAD(J); /* find record number 'i0' of index 'i' containing the indexed field's content in record 'r0' of 'nad.dat' Note that T0findNAD() does a safety check on the range of 'i0', so: */ if(i0 == 0) continue; /* index error, so move on to do the next index. T0findNAD() finds the FIRST exact match. However, if more than one record in 'nad.dat' contains the same content in the first 14 characters of the field pertaining to Index 'i', we could be collapsing Index 'i' over the wrong entry. We need to check that the 1st byte of the found index record 'i0' contains record number 'r0' of the 'nad.dat' record to be deleted. If it doesn't, we must step forward along Index 'i' to see if the next index entry to see if its first byte contains the same value as 'r0' and so on until we find the correct index entry. We don't need to check to see if subsequent index entries contain the required search term because if the index is good, the correct record will be found. 16384 equivalent 16-byte records of PEOPLE data in 'nad.dat' | number of the 1st index record of search range | | times 16 bytes per index record | | | */ int Q = 0x4000 + I0 << 4; // start byte of the required index while(i0 <= 255) { /* while we've not yet reached end of index block SEEK THE START BYTE OF INDEX RECORD 'i0' | file handle for 'nad.dat' | | start byte of the required index | | | number of the latest found index record | | | | times 16 bytes per record | | | | | */ fseek(FA,Q + (i0 << 4),SEEK_SET); /* record number in 'nad.dat' of the record to be deleted | record number of 'nad.dat' contained in the index entry | | */ if(r0 != fgetc(FA)) // if these are not the same, advance i0++; // to the next record 'i0' of Index 'i' } /* NOW CHECK THAT THE VALUE OF 'i0' WE END UP WITH IS WITHIN VALID RANGE first occupied record in 'nad.dat' | last occupied record in 'nad.dat' | | */ if(i0 < 1 || i0 > N0) { // if retrieved record number beyond valid range showMsg(9); // show the appropriate error message printf("Error in Record %3i of Index %i. Do REGEN\n",i0,i); continue; // move on to the next index } /* 3) COLLAPSE THE INDEX OVER THE INDEX RECORD TO BE OBLITERATED */ for(j = i0; j < N0; j++) { // for each of this index's records above 'i0' int k, y = Q + (j << 4); // start byte of record 'j' of index 'i' fseek(FA,y+16,SEEK_SET); // seek start byte of index record j+1 for(k = 0; k < 16; k++) // for each of the 16 bytes of the record st[k] = fgetc(FA); // put it into corresponding element of st[] fseek(FA,y,SEEK_SET); // seek start byte of index record 'j' for(k = 0; k < 16; k++) // for each of the 16 bytes of the record fputc(st[k],FA); // write it to corresponding byte of record } fputc(0,FA); // zero the record number [first byte] of record j+1 fflush(FA); // make sure this index is up to date on disk } } // NOTE: T0occu(0) will decrement 'N0' upon return /* THE 'KILL' BUTTON WAS CLICKED. This function marks the current record as unoccupied and removes its entries from the 4 indexes. Called from only one place in T0horizButClick(). */ void T0KILL() { if(a0 & 0x30) return; // exit if the LIST or DIARY button is on if(a0 & 2) // if the KILL button be bright a0 &= ~2; // dim it else { T0colIdx(); // collapse the indexes to remove this records entries T0occu(0); // set the current name & address record as unoccupied if(T0prev()) // if there exists a previous occupied record T0showNAD(); // display it showMsg(10); // show message "RECORD DELETED" a0 |= 2; // brighten the KILL button a0 &= ~0xFD; // dim all the other bottom buttons 11111101 } } /* GETS AND DISPLAYS THE NEXT OR PREVIOUS RECORD IN THE ASCII ORDER OF THE CURRENTLY SELECTED INDEX. Called only from 1 place each in T0NEXT() and T0PREV(). */ void T0nextprev(int d) { // d = +1 for NEXT and d = -1 for PREV int i; // loop index needs to be outside loop for(i = 0; i < 4; i++) // for each of the 4 index fields if(b0 & 1 << i) { // if this index is the active one I0 = i << 8; // rec num in 'nad.idx' of Record 0 of index 'i' break; // and break out of the for() loop } if(i < 4) { // provided index field number is 0,1,2 or 3 i0 += d; // increment/decrement the index record number if(d > 0) { // if d is +1 if(i0 > N0) i0 = 1; // wrap back to bottom of index if necessary } else { // otherwise 'd' must be -1, so if(i0 < 1) i0 = N0; // wrap back to top of index if necessary } /* SEEK THE FIRST BYTE OF THE NEXT RECORD IN INDEX 'i' file pointer [handle] for 'nad.idx' | skip the 262144 [0x40000] bytes of PEOPLE data in 'nad.dat' | | record number of Record Zero of Index 'i' | | | record number within Index 'i' | | | | multiply by 16 bytes per record | | | | | */ fseek(FA,0x4000 + I0 + i0 << 4,SEEK_SET); r0 = fgetc(FA); // get the corresponding 'nad.dat' record number T0showNAD(); // display the NEXT/PREV name & address in the index } } /* THE 'NEXT' BUTTON HAS BEEN CLICKED. If the last 'nad,dat' record was retrieved via an index search, the name & address next in the order of that index is retrieved and displayed; otherwise the next name & address in record [Ref number] order is retrieved and displayed. Called from 1 place only in T0horizButClick(). */ void T0NEXT() { if(a0 & 0x30) return; // exit if the LIST or DIARY button is on if(a0 & 0x100) { // if trawling is active r0++; // move on to the next record T0trawl(); // trawl for the next occurrence of the search term } else if(b0 == 0) { // if none of the 4 index search fields is selected if(T0next()) // if there exists a next occupied record T0showNAD(); // display it [record number r0 has been set by T0next()] } else // otherwise, if one of the 4 index fields is active T0nextprev(+1); // see function above a0 &= ~0xFB; // dim all other buttons on the bottom row } /* THE 'PREV' BUTTON HAS BEEN CLICKED. If the last 'nad,dat' record was retrieved via an index search, the previous name & address in the order of that index is retrieved and displayed; otherwise the previous name & address in record [Ref number] order is retrieved and displayed. Called from 1 place in T0horizButClick(). */ void T0PREV() { if(a0 & 0x30) return; // exit if the LIST or DIARY button is bright if(b0 == 0) { // if none of the 4 index search fields is selected if(T0prev()) // if there exists a previous occupied record T0showNAD(); // display it } else // otherwise, if one of the 4 index fields is active T0nextprev(-1); a0 &= ~0xF7; // dim all other bottom buttons 0xF7 = 11110111 } void T0LIST() { // THE 'LIST' BUTTON WAS CLICKED if(a0 & 0x20) return; // exit if DIARY button bright if(a0 & 0x10) { // if the LIST button is bright a0 &= ~0x10; // dim it if(a0 & 0x20) // if the DIARY button is bright T0diary(); // display the DIARY else { // otherwise if(r0 < 1) // in case no LIST item was selected r0 = 1; // default to record number 1 T0Show(); // display the current name & address panel } } else { // otherwise a0 |= 16; // brighten the LIST button if(c0 == 0) { c0 = 8; d0 = 3; } T0list(); // display the PEOPLE list } } void T0DIAR() { // THE 'DIARY' BUTTON WAS CLICKED if(a0 & 0x10) return; // exit if LIST button bright if(a0 & 0x20) { // if the DIARY button is bright a0 &= ~0x20; // dim it if(a0 & 0x10) // if the LIST button is bright T0list(); // display the PEOPLE list else { // otherwise if(r0 < 1) r0 = 1; T0Show(); // display the current name & address panel } } else { // otherwise a0 |= 0x20; // brighten the DIARY button if(c0 == 0) { c0 = 8; d0 = 3; } T0diary(); // display the DIARY } } /* 'NOTES' BUTTON CLICKED. This causes a notes file to be opened correspond- ing to the current name and address. Called only from T0horizButClick(). */ void T0NOTE() { char S[] = "gedit notes/N000.txt &", // template for the gedit command line *s = showInt(r0,3,1); // current record number a 3-digit string S[13] = s[0]; // embed 1st digit of the record number in the template S[14] = s[1]; // embed 2nd digit of the record number in the template S[15] = s[2]; // embed 3rd digit of the record number in the template system(S); // send the completed command to the terminal } /* ONE OF THE PEOPLE TAB'S CONTROL BUTTONS WAS CLICKED. Called from 1 place in MEH(). */ void T0horizButClick(int x) { showMsg(0); // clear any remaining message switch(x) { // switch according to button number case 0: T0NEW(); break; // NEW button was clicked case 1: T0KILL(); break; // KILL button was clicked case 2: T0NEXT(); break; // NEXT button was clicked case 3: T0PREV(); break; // PREV button was clicked case 4: T0LIST(); break; // LIST button was clicked case 5: T0DIAR(); break; // DIARY button was pressed case 6: T0NOTE(); break; // NOTES button was clicked case 7: helpPage("software/fep.html#people"); } T0Buts(); // re-display the buttons } /* Esc KEY [ESCAPE] PRESSED WHEN EDITING A NAME & ADDRESS FIELD Causes the edited version of the field to be discarded and the original content pulled again from disk and re-displayed. Called from only 1 place in T0putField() and the Keyboard Event Handler KEH(). */ void T0Esc() { if(a0 & 0x20) { // if we are in the DIARY sub-tab ea = 0; // switch off the line editor u0 = 0; // unmark any marked line v0 &= ~0xFF; // dim the vertical buttons T0diary(); // re-display the DIARY as was } else { b0 = 0; // clear field name colour states a0 &= ~0x100; // clear trawl mode T0drawBox(BLACK); // box field 'f0' in the given colour T0clearInside(); // background the inside of the editing box doCursor(0); // kill cursor XSetForeground(XD,XG,BLEEN); /* colour for the display lettering 0x200+ 00000000000000000000001000000000 search mode bit 0x400+ 00000000000000000000010000000000 REF entry mode bit 0x800= 00000000000000000000100000000000 selector mode bit */ if(a0 & 0xE00) { // if in search, select or ref modes a0 &= ~0xE00; // kill search, select and ref modes T0Show(); // re-display the original name & address } else { /* else we're escaping while in edit mode seek start byte of name & address field that's just been edited | file handle for 'nad.dat' | | start byte of field that has just been edited | | | */ fseek(FA,p0,SEEK_SET); // Get the old version of the edited field T0showField(f0); // from disk and redisplay it. } } ea = 0; // de-activate the line editor } /* PUT THE CONTENT OF THE LINE EDITOR ARRAY TO THE FIELD OF THE 'nad.dat' RECORD THAT HAS JUST BEEN EDITED. Called once from T0edField(). */ void T0putField() { if(en > L0[f0]) en = L0[f0]; /* cap content length of line editor field seek start byte of name & address field that's just been edited | file handle for 'nad.dat' | | start byte of field that has just been edited | | | */ fseek(FA,p0,SEEK_SET); /* content length of line editor field | file handle for 'nad.dat' | | */ fputc(en,FA); /* store the length of the field content in the first byte number of characters in the line editor character array content of the edited field | current character number being stored | | | file handle for 'nad.dat' | | | | */ for(int i = 0; i < en; i++) fputc(le[i],FA); // save content of edited line fflush(FA); // make sure the updated field is on disk now T0Esc(); // erase box if necessary and re-display field } /* FIND, RETRIEVE AND DISPLAY THE NAME AND ADDRESS RECORD CORRESPONDING TO A SEARCH TERM ENTERED INTO ONE OF THE INDEXED SEARCH FIELDS. Called from only one place in T0CR(). */ void T0search() { int x, i, f; if(f0 == 9) SUC(en,le); // allow upper case only for SEARCH NAME FS = FA; /* set index for strComp() number of the first record in the search range [record '0' of the index] | field number of the on-screen record | | the eighth field is the first indexable field | | | multiplied by 256 records per index | | | | */ I0 = f0 - 8 << 8; /* start byte of the index to be searched number of characters in the entered search term | */ for(i = 0; i < en; i++) // copy search term into st[] array st[i] = le[i]; // for T0findNAD() f = T0findNAD(en); /* get index rec num 'i0' of entered search term For return value, f: see T0findNAD() but it is not used here at the moment. index record number of the entered search term | if it is less than 1 | | OR beyond the highest occupied record | | | | */ if(i0 < 1 || i0 > N0) return; /* bail out if not within valid range SEEK START BYTE OF THE FOUND INDEX RECORD 'i0' WITHIN 'nad.dat' | file pointer [handle] for 'nad.idx' | | skip the 262144 [0x40000] bytes of PEOPLE data in 'nad.dat' | | | record number of Record Zero of Index 'i' | | | | record number within Index 'i' | | | | | multiply by 16 bytes per record | | | | | | */ fseek(FA,0x4000 + I0 + i0 << 4,SEEK_SET); /* if 'nad.dat' record number >= 1 | returns the number of the 'nad.dat' record of the matching term | | AND current highest occupied record | | | | set it as current record number | | | | | display name & address | | | | | | */ if((x = fgetc(FA)) > 0 && x <= H0) { r0 = x; T0showNAD(); } } /* SHIFT AN EDITED INDEX ENTRY FROM ITS ORIGINAL POSITION TO ITS NEW CORRECT POSITION WITHIN THE ALPHABETICAL [ASCII] ORDER OF THE INDEX. Called only from one place in T0edField(). */ void T0idxShft() { int i, // loop variable L, // valid length of field content from disk I = L0[f0]; /* field length of this field in the NAD record SEEK START BYTE OF CONTENT OF EDITED INDEXED FIELD IN 'nad.dat' RECORD | file handle for 'nad.dat' | | start byte of the current name & address record | | | off-set of this field within a name & address record | | | | number of indexed field [0 to 3] | | | | | */ fseek(FA,q0 + O0[f0],SEEK_SET); /* 1) LOAD THE ORIGINAL CONTENT OF THE FIELD BEFORE IT WAS EDITED */ L = fgetc(FA); // length of valid field content if(L < 0) L = 0; // if erroneously less than zero make it zero if(L > I) L = I; // if erroneously greater than field length for(i = 0; i < L; i++) // for each byte of the pre-edit version st[i] = fgetc(FA); /* pull it from the 'nad.dat' record into st[] 2) FIND THE FIELD'S POSITION 'a' WITHIN THE APPROPRIATE INDEX FILE: on-screen field number of the displayed 'nad.dat' record | the eighth field is the first indexable field | | multiplied by 256 records per index | | | */ I0 = f0 - 8 << 8; /* record zero of the index to be searched */ FS = FA; // set index for strComp() int f = T0findNAD(L); // find its record number 'i0' within index file if(f != 0) { printf("Err: can't find exact match. Do a REGEN\n"); return; } // T0findNAD() also displays an error message on the tab's message line. int a = i0; /* mark the index entry to be obliterated 3) FIND WHERE THE EDITED VERSION SHOULD BE PUT 'b' WITHIN THE INDEX FILE */ for(i = 0; i < en; i++) // copy the edited content of the field st[i] = le[i]; // into the search-term array st[] f = T0findNAD(en); // find its record number 'i0' within index file 'FS' int b = i0; /* mark index position to insert the edited entry 4) SHIFT THE INTERVENING BLOCK OF RECORDS UP OR DOWN OVER THE OLD ENTRY */ if(a < b) { // if old content lower in index than new content if(f != 3) // if search term not beyond end of index, b--; // back down one place for(i = a; i < b; i++) // Working forwards from bottom to top of block T0idxRecShft(+1,i); // shift record 'i+1' down to i-th position. } else if(a > b) // if old content higher up index than new content for(i = a; i > b; i--) // Working backwards from top to bottom of block T0idxRecShft(-1,i); /* shift Record 'i-1' up to i-th position. 5) WRITE THE EDITED VERSION OF THE FIELD INTO THE VACATED INDEX POSITION SEEK START BYTE OF THE INDEX RECORD ENTRY | file handle for 'nad.idx' | | skip over the 256 1024-byte PEOPLE data records | | | start byte of selected index | | | | record number within index to insert edited entry | | | | | times 16 bytes per record | | | | | | */ fseek(FA,0x4000 + I0 + b << 4,SEEK_SET); fputc(r0,FA); // 'nad.dat' record number to the new index entry position fputc(I,FA); // length of field content to the new index entry position for(i = 0; i < I; i++) // store the content of the Line Editor array fputc(le[i],FA); // in the appropriate index record fflush(FA); // make sure index file is up to date on disk } /* MAKE THE CONTENT OF THE SELECTOR KEY FIELD CONTAIN ONLY CHARACTERS FROM 0 TO 9 AND FROM A TO Z. Called only from T0edField() below. */ void T0key() { int i, I = L0[12]; // length of selector code field in NAD record for(i = 0; i < en; i++) // for each character of the valid content le[i] = AZ09(le[i]); // convert letter to upper case etc. while(i < I) // fill the remainder of the field le[i++] = '0'; // with '0' characters en = I; // set length of field to the full capacity } /* CARRIAGE-RETURN [ENTER] KEY HAS BEEN PRESSED AFTER A FIELD OF A NEW RECORD HAS BEEN ENTERED OR THE FIELD OF AN EXISTING RECORD HAS BEEN EDITED Called only from one place in T0CR() below. */ void T0edField() { if(f0 == 9) // if just edited the SEARCH NAME field SUC(en,le); // ensure it is all in upper case letters only else if(f0 == 12) // if just edited the SELECTOR KEY field T0key(); // ensure it contains only 0 to 9 and A to Z if(f0 > 7 && f0 < 12) // if just edited an indexable field T0idxShft(); // shift its index entry to its new correct place T0putField(); /* store field in 'nad.dat'; erase box; redisplay content IF WE'RE [STILL] ENTERING A NEW NAME & ADDRESS | and we haven't yet filled in the last field | | */ if(a0 & 1 && f0 < 12) { mx = 198; /* set mouse x-coordinate for data entry fields y-coord of the first [top] field | plus the incremented current field number | | times 20 pixels inter-field drop | | | */ my = 58 + ++f0 * 20; // mouse y-coordinate for next field if(f0 < 8 && f0 > 11) // if not an indexable field for(int i = 0; i < 48; i++) // clear the line editor array le[i] = 0; T0click(); // do a virtual 'click' on the field } else { // otherwise, we've finished entering the new name & address ea = 0; // so disable the line editor a0 &= ~1; // and dim the NEW button } } /* CARRIAGE-RETURN [ENTER] PRESSED WHEN EDITING A NAME & ADDRESS FIELD Called from only 1 place in the Keyboard Event Handler KEH(). */ void T0CR() { if(a0 & 0x20) { // if we're in the DIARY sub-tab if(v0 & 7) // if the diary's NEW, ED-PREV or ED-DONE button is bright T0diaryPostEdit(); } else if(a0 & 0x800) // if SELECTOR KEY search mode is active T0selKey(); // list names & addresses that match entered key settings else if(a0 & 0x400) { // if in Reference Number entry mode ea = 0; // disable the line editor r0 = atoi(le); // convert line editor content to integer 'r0' T0showNAD(); // display the name & address record 'r0' } else if(a0 & 0x200) { // if we're in search mode ea = 0; // disable the line editor if(f0 == 0) { // if the search term was entered in Field Zero r0 = 1; // set to start with first record T0trawl(); // trawl for it within all non-indexed fields } else // else use the appropriate index to find the search term T0search(); // find, retrieve and display the name and address record a0 &= -0x200; // we've finished the search, so revert to editing mode } else // else just modified one of the name & address fields T0edField(); // so drop down to next field for editing T0Buts(); // re-display the button states } // SCROLLING OPERATION dispatcher FOR TAB 0 void T0scroll() { switch(D0) { case 1: T0listScroll(); break; case 2: T0diaryScroll(); } } // EXPOSURE/RE-EXPOSURE dispatcher FOR TAB 0 void T0show() { if(a0 & 0x10) T0list(); else if(a0 & 0x20) T0diary(); else T0Show(); } // --------------------FUNCTIONS PERTAINING TO TAB 7 MAINT ------------------- /* BACKUP OR RESTORE A SPECIFIED DATA FILE: 'nad.dat', 'diary.dat', 'crop.dat', fin.dat'. */ void T7bkup(int n) { // n = vertical button number char *S[] = { "backup/nad.dat", // backup file for name & address data + indexes "backup/diary.dat", // backup file for diary data + indexes "backup/crop.dat", // backup file for crop data + indexes "backup/fin.dat" // backup file for transaction data + indexes }, *s[] = {"data/nad.dat","data/diary.dat","data/crop.dat","data/fin.dat"}, *T; // for "w" [write] or "r" [read] file mode if(a7 & 1) // if we're in the BACKUP sub-tab T = "w"; // set for writing to the backup file else // else, we're in the RESTORE sub-tab T = "r"; // so set for reading from the backup file int N[] = { 274704, // size [in bytes] of nad.dat 20480, // size [in bytes] of diary.dat 33312, // size [in bytes] of crop.dat 36864 // size [in bytes] of fin.dat }, I = N[n]; // get the length [in bytes] of the selected file FILE *Q[4] = {FA,FB,FC,FD}, // file handles for the 4 data files *F = Q[n], // data file corresponding to clicked button *G = fopen(S[n],T), // open the backup file for read or write *H, // 'read-from' file handle *J; // 'write-to' file handle if(a7 & 1) { // if we're in BACKUP sub-tab set for fseek(F,0,SEEK_SET); // seek start byte of data file to be backed up H = F; // set to read from the data file J = G; // set for writing to the backup file } else { // else, we're in the RESTORE sub-tab fclose(F); // close the data file in "r+" [read/modify] mode F = fopen(s[n],"w"); // and re-open it in "w" [create/write] mode H = G, // set to read from the backup file J = F; // set for writing to the data file } for(int i = 0; i < I; i++) // for each byte of the file size fputc(fgetc(H),J); // get from input file; write to output file fclose(G); // close the backup file if(a7 & 2) { // if in RESTORE sub-tab fclose(F); // close data file in "w" [create/write] mode F = fopen(s[n],"r+"); // and re-open it in "r+" [read/modify] mode } } void T7bakup(int i) { showMsg(0); if(i < 4) T7bkup(i); // if button 0,1,2,3 was clicked do backup else if(i == 4) // else if 'backup all' was clicked for(int j = 0; j < 4; j++) T7bkup(j); // back up each of the 4 data files in turn else if(i == 7) { // the HELP key was clicked helpPage("software/fep.html#backup"); return; } showMsg(26); } /* INDEX INTEGRITY CHECKER. Checks that all the elements of each of the indexes are in ascending ASCII casting order. Note that the indexes [each comprising 256 16-byte records] follow on in the main data file after the end of the 256 records containing the data itself. Called from only one place in T0vBbutClick(). handle of index being checked e.g. FA,FB,FC,FD | number of output file name | | off-set [in bytes] to start of indexes | | | number of indexes | | | | highest occupied record | | | | | */ void T7integ(FILE *F, int n, int o, int I, int J) { int i, // indicates which of the 4 index files is being checked j, // indicates which of its records is being checked k, // indicates which of the 14 possible characters of the text K, // number of valid characters in the current index text field f = 0, // overall integrity failure flag R = 1, // record number of main data record pointed to by index entry L = 14; /* last time's value of K */ char *N[] = { // OUTPUT FILES FOR INDEX INTEGRITY TEST "test/nadidx.txt", // n=0 "test/diaryidx.txt", // n=1 "test/cropidx.txt", // n=2 "test/finidx.txt" // n=3 }, S[16]; // to hold the current index element for testing FILE *G = fopen(N[n],"w"); // Human-readable index file for(i = 0; i < I; i++) { // for each of the relevant indexes /* START BYTE OF SORT FIELD OF FIRST RECORD OF INDEX 'i' IN 'nad.dat' | off-set [in bytes] to start of indexes | | plus hex 16 bytes to get to start byte of index Record 1 | | | plus the index number | | | | times index size 256 * 16 = 4096 [0x1000] | | | | | */ int B = o + 0x10 + (i << 12); for(k = 0; k < 14; k++) // clear the S[] character array st[k] = 0; // as first time's 'previous' element for(j = 1; j <= J; j++) { // for each record in this index fseek(F,B,SEEK_SET); // seek the start byte of the index record char *s = getIdxRec(F), // get the index record *S = s + 2; // pointer to start of search term text R = (unsigned)s[0]; // number of the main data record K = (unsigned)s[1]; // length of the search term content /* file handle for human-readable output text file | data record number | | content length of search string | | | search term string | | | | */ fprintf(G,"%3i %2i %s",R,K,S); if(K > L) K = L; // test as far as length of shortest field int g = 0; // flag to indicate an error in an individual index record for(k = 0; k < K; k++) { // for each character in the search text if(S[k] > st[k]) break; // new character higher than old character if(S[k] < st[k]) { // at the first out-of-order pair encountered showMsg(11); // display the index integrity error message printf("Index intergrity error: Ind %i, Ele %i Chr %i\n",i,j,k); f = 1; g = 1; break; // set the error flags and break out } } if(g == 1) fprintf(G," Index error: Ind %i, Ele %i Chr %i",i,j,k); for(k = 0; k < K; k++) // make current element st[k] = S[k]; // next time's previous element L = K; // make last time's content length this time's B += 16; // advance to the next record fprintf(G,"\n"); // end of record line } fprintf(G,"\n"); // miss a line between indexes } if(f == 0) showMsg(12); // index integrity OK message fclose(G); // close the index print file } /* RESET THE DIARY TO TEST DATA The file 'diary.dat' contains the following: 256 * 16-byte records for the diary itself 256 * 16-byte records for the PREV dates index 256 * 16-byte records for the NEXT dates index 256 * 16-byte records for the DAYS to go index 256 * 16-byte records for the NAD REF index --- 1280 * 16 = 20480 bytes total Format of a DIARY record: 4-byte integer:PREV date of contact | 4-byte integer:NEXT date of proposed contact | | 4-byte integer:DAYS to go to NEXT proposed contact | | | 4-byte integer: 'nad.dat' REF of person to whom | | | | the diary entry pertains 1111000011110000 */ void T7diaryReset() { int N[31][4] = { /* PREV NEXT DAYS NAD REF TEST DATA | | | | */ {20230115,20230503,30,04}, {20230116,20230506,14,11}, {20230118, 0,27,07}, {20230119,20230519,16,13}, {20230123,20230525,06,05}, {20230128, 0,03,07}, {20230204,20230616,19,12}, {20230211,20230618,33,11}, {20230225, 0,45,07}, {20230227, 0,23,06}, {20230306,20230627,18,03}, {20230319,20230707,30,02}, {20230320,20230717,22,07}, {20230322,20230728,05,02}, {20230324,20230731,23,11}, {20230125, 0,13,07}, {20230329,20230813,18,06}, {20230404,20230823,07,03}, {20230405, 0,24,02}, {20230406,20230904,27,11}, {20230408,20230913,13,03}, {20230409, 0,11,07}, {20230410,20230926,04,12}, {20230412,20230930,12,04}, {20230415,20231002,22,06}, {20230419, 0,32,10}, //vc {20230423,20231005,12,07}, {20230424,20231023,04,01}, {20230425,20231024,29,04}, {20230427, 0,10,05}, //jj {20230430,20231101,20,10} //vc }, i; // INSERT THE TEST DATA INTO IT fseek(FB,0,SEEK_SET); fputc(31,FB); // current number of items in the diary fputc(0,FB); // say that all indexes need sorting fseek(FB,16,SEEK_SET); // seek start byte of Record 1 for(i = 0; i < 31; i++) { // for each of the first 31 records putInt(N[i][0],FB); // PREV date putInt(N[i][1],FB); // NEXT date putInt(N[i][2],FB); // DAYS putInt(N[i][3],FB); // NAD REF } fflush(FB); } /* TRANS RESET Record Format: all 4-byte integers 0000 transaction date 0000 transaction type 0000 ref number of client in 'nad.dat' 0000 ref number of product in 'crop.dat' 0000 price 0000 quantity 0000 date paid */ void T7transReset() { int N[31][8] = { // 31 records {20230228, 1, 2, 3, 218, 230, 20230316}, // each field is 4 bytes {20230119, 1, 4, 8, 200, 300, 20230530}, {20230127, 1, 5, 12, 100, 500, 20230722}, {20230202, 1, 4, 22, 250, 200, 20230511}, {20230225, 2, 10, 16, 300, 100, 20230428}, {20230304, 2, 3, 13, 50, 200, 20230530}, {20230316, 1, 1, 5, 120, 300, 20230627}, {20230319, 1, 11, 3, 170, 400, 20230508}, {20230425, 0, 4, 9, 30, 300, 20230511}, {20230428, 1, 9, 24, 100, 100, 20230508}, {20230503, 1, 7, 27, 150, 150, 20230627}, {20230508, 2, 2, 17, 180, 300, 20230512}, {20230511, 1, 8, 7, 40, 500, 20230807}, {20230512, 2, 7, 15, 440, 300, 20230630}, {20230520, 1, 12, 4, 320, 300, 20230602}, {20230530, 3, 9, 6, 220, 250, 20230814}, {20230602, 3, 6, 13, 260, 240, 20230722}, {20230627, 2, 5, 19, 280, 70, 20230806}, {20230630, 1, 4, 20, 180, 160, 20230804}, {20230715, 3, 4, 27, 330, 300, 20230915}, {20230722, 1, 7, 15, 190, 200, 20230814}, {20230727, 2, 4, 5, 80, 400, 20230827}, {20230804, 1, 3, 7, 100, 100, 20230915}, {20230806, 4, 3, 2, 200, 350, 20230821}, {20230807, 1, 4, 1, 100, 380, 0}, {20230814, 2, 13, 11, 200, 330, 0}, {20230821, 2, 4, 16, 240, 170, 0}, {20230827, 2, 8, 8, 260, 220, 0}, {20230915, 1, 7, 4, 160, 180, 0}, {20230925, 4, 3, 29, 70, 300, 0}, {20231025, 1, 4, 1, 140, 170, 0}, }, i, j; fseek(FD,0,SEEK_SET); fputc(31,FD); // number of records on file in byte 0 for(i = 1; i < 32; i++) // zero the 31 bytes that remain in fputc(0,FD); // record zero for(i = 1; i < 32; i++) { // for each of records 1 to 31 for(j = 0; j < 7; j++) // put the appropriate line of putInt(N[i-1][j],FD); // data from the above array putInt(0,FD); // put zero in the last int of the record } for(i = 0; i < 7168; i++) // for 256-32=224 records of 32 bytes fputc(0,FD); // zero the 7168 bytes for(i = 0; i < 28672; i++) // 7 indexes of 256 records each fputc(0,FD); // at 16 bytes per record = 28672 bytes fflush(FD); } /* A TEMPORARY DEVELOPMENT FUNCTION. Resets the file system back to test data conditions. Called by T0vButClick() and T0DELL(). */ void T7rset() { switch(e7) { // SWITCH ON VERTICAL BUTTON COLUMN case 1: // reset the PEOPLE data to test status fseek(FA,0,SEEK_SET); // seek the start byte of 'nad.dat' fputc(255,FA); // mark records 0 to 7 as occupied fputc(255,FA); // mark records 8 to 15 as occupied fputc(255,FA); // mark records 16 to 23 as occupied for(int i = 0; i < 29; i++) fputc(0,FA); // mark all other records as unoccupied fflush(FA); // flush this situation to disk break; case 2: T7diaryReset(); break; // reset the DIARY data to test status case 4: // reset the CROPS data to test status fseek(FC,0,SEEK_SET); // seek the start byte of 'crop.dat' fputc(255,FC); // set record zero + records 00 to 07 as occupied fputc(255,FC); // set record zero + records 07 to 15 as occupied fputc(255,FC); // set record zero + records 15 to 23 as occupied fputc(255,FC); // set record zero + records 15 to 23 as occupied fputc(224,FC); // set record zero + records 15 to 23 as occupied for(int i = 5; i < 32; i++) fputc(0,FC); // set records 6 to 255 as unoccupied fputc(34,FC); // number of occupied records fputc(34,FC); // highest occupied record fputc(31,FC); // 're-indexing necessary' bits fflush(FC); break; case 8: T7transReset(); // reset the TRANS data to test status } } void T7reset(int i) { showMsg(0); if(i < 4) T7rset(i); // if button 0,1,2,3 was clicked do backup else if(i == 4) // else if 'backup all' was clicked for(int j = 0; j < 4; j++) T7rset(j); // back up each of the 4 data files in turn else if(i == 7) { // the HELP key was clicked helpPage("software/fep.html#reset"); return; } showMsg(13); } void T7rgen() { int i; switch(f7) { // switch on REGEN sub-tab's vertical buttons state case 1: T0regen(); break; // REGENERATE ALL THE NAD INDEXES case 2: // REGENERATE ALL THE DIARY INDEXES fseek(FB,3,SEEK_SET); // seek the diary's re-sort needed flags byte fputc(0,FB); // reset all 8 bits to zero for(i = 0; i < 3; i++) // for each of the 3 diary indexes T0diarySort(i); // regenerate it and sort it break; case 4: // REGENERATE ALL THE CROP INDEXES fseek(FC,35,SEEK_SET); // seek the crop's re-sort needed flags byte fputc(0,FC); // reset all 8 bits to zero for(i = 0; i < 5; i++) // for each of the 5 crop indexes T5cropSort(i); // regenerate it and sort it showMsg(14); // show 'index regeneration completed' message break; case 8: // REGENERATE ALL THE TRANSaction INDEXES fseek(FD,3,SEEK_SET); // seek the trans re-sort needed flags byte fputc(0,FD); // reset all 8 bits to zero for(i = 0; i < 7; i++) // for each of the 7 transaction indexes T6transSort(i); // regenerate it and sort it showMsg(14); // show 'index regeneration completed' message break; } } void T7regen(int i) { showMsg(0); // clear the message field if(i < 4) T7rgen(i); // if button 0,1,2,3 was clicked do backup else if(i == 4) // else if 'backup all' was clicked for(int j = 0; j < 4; j++) T7rgen(j); // back up each of the 4 data files in turn else if(i == 7) { // the HELP key was clicked helpPage("software/fep.html#regen"); return; } showMsg(14); } /* DRAW HORIZONTAL ROW OF CONTROL BUTTONS ACROSS BOTTOM OF THE MAINTENANCE TAB. Called from 1 place each in T7show() and T7horizButClick(). */ void T7showHorizButs() { showButs(); // show 8 blank buttons across the bottom of the tab area const char // annotate the horizontal row of control buttons *A[] = {"BACKUP","RESTORE","INTEG","RESET","REGEN","SPARE","SPARE","HELP"}; const int a[] = {9,6,12,12,12,12,12,15}, // horizontal offsets for word starts b[] = {6,7, 5, 5, 5, 5, 5, 4}; // number of letters in each word int h = 17; // x coordinate of button block for(int i = 0; i < 8; i++) { // for each of the 8 buttons: if(a7 & 1 << i) // if this button is 'on' XSetForeground(XD,XG,WHITE); // set colour for bright lettering else XSetForeground(XD,XG,GREY); // set colour for dull lettering XDrawString(XD,XW,XG,h + a[i],377,A[i],b[i]); h += 59; // move across to next button } } /* DISPLAY THE VERTICAL COLUMN OF BUTTONS IN THE RAIN SUB-TAB. Called from T2showGraph(), T2showRainGraph(), T2showRainTank() */ void T7showVertiButs() { char // annotate the vertical row of control buttons *A[] = {"PEOPLE","DIARY","CROPS","TRANS","ALL","SPARE","SPARE","HELP"}; int a[] = { 9,12,12,12,18,12,12,15}, // horizontal offsets for word starts b[] = { 6, 5, 5, 5, 3, 5, 5, 4}, // number of letters in each word h = 99, // y coordinate of button block g = 0; switch(a7) { case 1: g = b7; break; case 2: g = c7; break; case 4: g = d7; break; case 8: g = e7; break; case 16: g = f7; } for(int i = 0; i < 8; i++) { // for each of the 8 buttons: XSetForeground(XD,XG,DARK); // shade for buttons' background XFillRectangle(XD,XW,XG,17,h-15,53,21); if(g & 1 << i) // if this button is bright [on] XSetForeground(XD,XG,WHITE); // set colour for bright lettering else // otherwise XSetForeground(XD,XG,GREY); // set colour for dull lettering XDrawString(XD,XW,XG,17+a[i],h,A[i],b[i]); h += 27; // move across to the next button } } void T7showTab() { char *A[5] = { "DATA BACKUP FACILITY ", "RESTORE FROM BACKUP ", "INDEX INTEGRITY CHECKER", "RESET BACK TO TEST DATA", "REGENERATE THE INDEXES " }, *B[5][5] = { { "Backup the PEOPLE data only.", "Backup the DIARY data only.", "Backup the CROPS data only.", "Backup the TRANSactions data only.", "Backup ALL data." }, { "Restore the PEOPLE data only.", "Restore the DIARY data only.", "Restore the CROPS data only.", "Restore the TRANSactions data only.", "Restore ALL data." }, { "Check integrity of the 4 name & address indexes. nadidx.txt", "Check integrity of the 3 diary indexes. diaryidx.txt", "Check integrity of the 5 crops indexes. cropidx.txt", "Check integrity of the 7 transactions indexes. finidx.txt", "Index integrity can only be checked one index at a time." }, { "Reset the PEOPLE file back to test data.", "Reset the DIARY back to test data.", "Reset the CROPS file back to test data.", "Reset the TRANSactions file back to test data.", "Reset all files back to test data." }, { "Regenerate the PEOPLE indexes.", "Regenerate the DIARY indexes.", "Regenerate the CROPS indexes.", "Regenerate the TRANSactions indexes.", "Regenerate all indexes." } }, *C[5] = { "The backup data is put in the directory [folder] \"/backup\".", "The backup data is got from the directory [folder] \"/backup\".", "The results are in the files named on the right in \"/test\".", "BACKUP all data before you click any of these buttons.", "" }; int a[] = {20,19,23,23,22}, // line lengths for the TAB titles b[5][5] = { {28,27,27,34,16}, // line lengths for BACKUP tab annotations {29,28,28,35,17}, // line lengths for RESTORE tab annotations {62,64,63,62,56}, // line lengths for INTEG tab annotations {40,34,39,46,34}, // line lengths for REGEN tab annotations {30,29,29,36,23} // line lengths for RESET tab annotations }, c[5] = {59,61,59,54,0}, // line lengths of bottom messages v = 98, // y-coordinate of first annotation line i, j; // loop variables for(i = 0; i < 5; i++) // form the button number from the button code if(a7 == 1 << i) break; // break with found button number clearTab(); XSetForeground(XD,XG,YELLOW); // colour for the lettering XDrawString(XD,XW,XG,17,70,A[i],23); XSetForeground(XD,XG,BLEEN); // colour for the lettering for(j = 0; j < 5; j++) { XDrawString(XD,XW,XG,87,v,B[i][j],b[i][j]); v += 27; } XDrawString(XD,XW,XG,17,315,C[i],c[i]); showMsg(0); T7showVertiButs(); } void T7spare() { char *S[] = { "This is a SPARE TAB used for performance monitoring of ", "the software during initial development and during cus-", "tomisation for a particular application site. It's not ", "implemented in this demonstration version. " }; clearTab(); int y = 120; XSetForeground(XD,XG,BLEEN); for(int i = 0; i < 4; i++) XDrawString(XD,XW,XG,90,y+=19,S[i],55); showMsg(0); } /* MOUSE CLICK HANDLER FOR TAB 6. Used to determine which button in the vert- ical column was clicked. Called only from T6click() below. */ void T7vClick() { if(mx < 17 || mx > 69) return; // exit if outside horizontal buttons limits int i, y = 84; // height [y-coordinate] of top of top button for(i = 0; i < 8; i++) { // for each of the 8 vertical buttons if(my > y && my < y+21) // if mouse within vertical limits of this break; // button, break out of loop: i = button number y += 27; // drop down to the next button } if(i > 7) return; // click was above top or below bottom button switch(a7) { // SWITCH ON SUB-TAB BUTTON CODE case 1: // THE BACKUP SUB-TAB b7 &= ~0xFF; // clear the backup control buttons b7 |= 1 << i; // illuminate the clicked vertical button T7bakup(i); // backup the selected file(s) break; case 2: // THE RESTORE SUB-TAB c7 &= ~0xFF; // clear the restore control buttons c7 |= 1 << i; // illuminate the clicked vertical button T7bakup(i); // backup the selected file(s) break; case 4: // THE INTEG SUB-TAB d7 &= ~0xFF; // clear the index integrity check buttons d7 |= 1 << i; // illuminate the clicked vertical button switch(i) { // SWITCH ON BUTTON NUMBER 0 TO 7 case 0: T7integ(FA,0,0x40000,4,N0); break; // PEOPLE case 1: T7integ(FB,1, 0x1000,3,N8); break; // DIARY case 2: T7integ(FC,2, 0x4000,5,N5); break; // CROPS case 3: T7integ(FD,3, 0x2000,7,N6); break; // TRANSactions case 7: // the HELP key was clicked helpPage("software/fep.html#integ"); } break; case 8: // THE REGEN SUB-TAB e7 &= ~0xFF; // clear the REGENerator control buttons e7 |= 1 << i; // illuminate the clicked vertical button if(i < 5) T7reset(i); // if one of buttons 0,1,2,3 clicked else if(i ==7) // the HELP key was clicked helpPage("software/fep.html#regen"); break; case 16: // THE RESET SUB-TAB f7 &= ~0xFF; // clear the reset control buttons f7 |= 1 << i; // illuminate the clicked vertical button if(i < 5) T7regen(i); // if one of buttons 0,1,2,3 clicked else if(i ==7) // the HELP key was clicked helpPage("software/fep.html#reset"); } T7showVertiButs(); } /* HANDLES CLICKS TO THE 8 BOTTOM BUTTONS OF THE TRADE TAB [TAB 6] Called from only one place in MEH(). */ void T7horizButClick(int i) { // called from only 1 place in MEH() clearTab(); a7 &= ~0xFF; // kill all buttons a7 |= 1 << i; // illuminate the clicked horizontal button switch(i) { case 0: T7showTab(); break; // BACUP case 1: T7showTab(); break; // RESTORE case 2: T7showTab(); break; // INTEG case 3: T7showTab(); break; // RESET case 4: T7showTab(); break; // REGEN case 5: T7spare(); break; // SPARE case 6: T7spare(); break; // SPARE case 7: // HELP helpPage("software/fep.html#maint"); } T7showHorizButs(); // re-display the buttons } /* THE MOUSE WAS CLICKED WITHIN THE MAINT TAB. Called from 2 places in MEH(). */ void T7click() { T7vClick(); } /* SETS UP THE TEXT AND BUTTONS FOR THE ADMIN TAB. This is a dummy function informing the user that this Tab's functionality is not implemented in this demo version of the program. Called from 1 place in showTab(). */ void T7show() { switch(a7) { case 1: T7showTab(); break; case 2: T7showTab(); break; case 4: T7showTab(); break; case 8: T7showTab(); break; case 16: T7showTab(); break; case 32: T7spare(); break; case 64: T7spare(); } T7showHorizButs(); // display the tab's buttons } // TAB DISPLAY DISPATCHER ---------------------------------------------------- /* DISPLAY ALL THE TITLES, BUTTONS AND DEFAULT IN-FILL FOR THE CURRENT TAB. The current tab is specified by the numeric value of the global variable 'tb'. Called from only 1 place each in main() and MEH(). */ void showTab() { const char *BA[8] ={"PEOPLE","LAND","WATER","POWER","EQUPT","PROJS","TRADE","MAINT"}; const int NC[8] = {6, 4, 5, 5, 5, 5, 5, 5}, // number of chars in each annotation NB[8] = {9,15,12,12,12,12,12,12}; // x-bias for each annotation XSetForeground(XD,XG,DARK); // shade for button background int h = 17, i, C; // set y-coordinate to left margin for(i = 0; i < 8; i++) { // draw each button XFillRectangle(XD,XW,XG,h,17,53,21); h += 59; // move across to the next button } h = 17; // reset to the left margin for(i = 0; i < 8; i++) { // annotate each button if(tb == i) // If this is the currently selected tab C = WHITE; // annotate it in bright white lettering /* else if we're in the PEOPLE tab and the PEOPLE 'alert flag' is set or if we're in the PROJS tab and the PROJS 'alert flag' is set: display the button text in RED; otherwise display it in GREY. */ else if(i == 0 & V8 || i == 5 & V5) C = RED; else C = GREY; XSetForeground(XD,XG,C); // annotate the button XDrawString(XD,XW,XG,h + *(NB+i),32,*(BA+i),*(NC+i)); h += 59; // move across to the next button } clearTab(); XSetForeground(XD,XG,BLACK); // clear Tab area XFillRectangle(XD,XW,XG,17,38,484,383); switch(tb) { // switch according to tab number case 0: T0show(); break; // paint the PEOPLE tab case 1: T1show(); break; // paint the LAND tab case 2: T2show(); break; // paint the WATER tab case 3: T3show(); break; // paint the POWER tab case 4: T4show(); break; // paint the EQUPT tab case 5: T5show(); break; // paint the PRODS tab case 6: T6show(); break; // paint the TRADE tab case 7: T7show(); // paint the MAINT tab } } // MOUSE EVENT HANDLERS------------------------------------------------------- /* MOUSE EVENT HANDLER. Handles clicks of the left mouse button [Button 1]. The x and y coordinates of the click-point within the application window are already in global variables mx and my. Called only from one place in main()'s event loop. */ void MEH() { int i, // loop variable x = 1, // indicates a click in the unoccupied area of the tab y = 0; // left edge of each button in turn if(my > 16 && my < 38) // if click was on one of the Tab buttons x = 0; // indicates that a Tab button was clicked else if(my > 361 && my < 383) // if click was on one of the Bottom buttons x = 2; // indicates a control button was clicked for(i = 0; i < 8; i++) { // for each button if(mx > 16 + y && mx < 70 + y) // if click was within a button break; // break out of loop y += 59; // move across to the next button } switch(x) { // switch according to type of item clicked case 0: // A TAB BUTTON WAS CLICKED tb = i; // set Tab Number showTab(); // display the Tab's content break; case 1: // THE CLICK OCCURRED IN SOME OTHER PART OF THE TAB AREA switch(tb) { case 0: T0click(); break; // clicked in another part of PEOPLE Tab case 1: T1click(); break; // clicked in another part of LAND Tab case 2: T2click(); break; // clicked in another part of WATER Tab case 3: T3click(); break; // clicked in another part of POWER Tab case 4: T4click(); break; // clicked in another part of EQUPT Tab case 5: T5click(); break; // clicked in another part of PRODS Tab case 6: T6click(); break; // clicked in another part of TRADE Tab case 7: T7click(); // clicked in another part of MAINT Tab } break; case 2: // A CONTROL BUTTON WAS CLICKED switch(tb) { // 'i' is button number left to right: 0 to 7 case 0: T0horizButClick(i); break; // PEOPLE Tab's button click handler case 1: T1horizButClick(i); break; // LAND Tab's button click handler case 2: T2horizButClick(i); break; // WATER Tab's button click handler case 3: T3horizButClick(i); break; // POWER Tab's button click handler case 4: T4horizButClick(i); break; // EQUPT Tab's button click handler case 5: T5horizButClick(i); break; // PRODS Tab's button click handler case 6: T6horizButClick(i); break; // TRADE Tab's button click handler case 7: T7horizButClick(i); // MAINT Tab's button click handler } } } void MWup() { // MOUSE WHEEL UP. Called only from 1 place in main() switch(tb) { case 0: mw = 2; T0scroll(); break; case 1: mw = 2; break; case 2: mw = 2; break; case 4: mw = 2; break; case 5: mw = 2; T5cropScroll(); break; case 6: mw = 2; T6mouseWheel(); } } void MWdn() { // MOUSE WHEEL DOWN. Called only from 1 place in main() switch(tb) { case 0: mw = 1; T0scroll(); break; case 1: mw = 0; break; case 2: mw = 0; break; case 4: mw = 0; break; case 5: mw = 1; T5cropScroll(); break; case 6: mw = 1; T6mouseWheel(); } } // ------------------------ KEYBOARD EVENT HANDLERS -------------------------- /* [RE]DISPLAY TEXTUAL CONTENT OF LINE EDITOR FIELD Called from 1 place each in bksp(), Del(), lineEditor(). */ void showLE() { int d = 50; XSetForeground(XD,XG,DARK); // clear box switch(tb) { case 0: if(a0 & 0x20) // if in the DIARY sub-tab XFillRectangle(XD,XW,XG,ex-3,ey-12,52,17); else XFillRectangle(XD,XW,XG,ex-3,ey-12,T0BIW(f0),17); break; case 5: d = 50; // most fields are 50 pixels wide if(f5 == 0 && !(b5 & 1)) // but the crop name field d = 190; // is 190 pixels wide XFillRectangle(XD,XW,XG,ex-2,ey-12,d,14); break; case 6: XFillRectangle(XD,XW,XG,ex-2,ey-12,52,14); // paint background } XSetForeground(XD,XG,WHITE); // display text in WHITE XDrawString(XD,XW,XG,ex,ey,le,en); doCursor(1); } void CR() { // CARRIAGE-RETURN EVENT DISPATCHER switch(tb) { case 0: T0CR(); break; case 5: T5cropCR(); break; case 6: T6transCR(); } } void Esc() { // ESCAPE KEY EVENT DISPATCHER switch(tb) { case 0: T0Esc(); break; case 5: T5cropEsc(); break; case 6: T6transEsc(); break; } } /* WHEN THE LEFT-ARROW OR THE RIGHT-ARROW KEY IS PRESSED Called only from one place in KEH(). */ void leftArrow() { doCursor(0); /* erase the cursor from its old position /* decremented cursor position [move it left] | position beyond end of content | | */ if(--ec < 0) ec = en; /* if cursor was already at beginning of the field, loop back round to the end of the line content. */ doCursor(1); // show the cursor in its new position } void rightArrow() { doCursor(0); /* erase the cursor from its old position /* incremented cursor position [move it right] | position beyond end of content | | put cursor at beginning of line | | | */ if(++ec > en) ec = 0; /* if cursor was already at the end of the line content, loop forwards round to the beginning. */ doCursor(1); // show the cursor in its new position } // WHEN THE 'Backspace' KEY IS PRESSED: Called only fom 1 place in KEH(). void bkSp() { doCursor(0); /* erase the cursor if the cursor is not at the beginning of the line editor field | and there is content at or beyond it | | current position of cursor within line editor field | | | current number of characters in the field's content | | | | */ if(ec > 0 && ec <= en) { ec--; /* Move the cursor one place left then pull all characters, at or beyond where the cursor was, back one place left. current position of cursor | number of characters of content in the line | | current character | | | next character | | | | */ for(int i = ec; i < en; i++) le[i] = le[i + 1]; en--; // decrement the field's content length le[en] = (char)0; // terminate shortened field content string showLE(); // display the field's textual content } } // WHEN THE 'Delete' KEY IS PRESSED: Called only from 1 place in KEH(). void Del() { if(ec >= en) return; /* exit if cursor at or beyond current end of content PULL ALL CHARACTERS, AT OR BEYOND THE CURSOR, BACK ONE PLACE LEFT. current position of cursor | number of characters of content in the line | | current character | | | next character | | | | */ for(int i = ec; i < en; i++) le[i] = le[i + 1]; le[en] = 0; // terminate shortened field content string en--; // decrement the field's content length showLE(); // display textual content of le[] field } // WHEN 'Home' or 'End' KEY IS PRESSED: Called only from 2 places in KEH(). void homeEnd(int x) { doCursor(0); // erase the cursor at its old position ec = x; // set new position of cursor doCursor(1); // display it in its new position } void PgUpDn(int x) { // PgUp or PgDn KEY PRESSED switch(tb) { case 0: T0PgUpDn(x); break; case 5: T5PgUpDn(x); break; case 6: T6PgUpDn(x); } } /* LINE EDITOR: All printable characters are received by this Line Editor which places each, as it is received, in its appropriate place within the Line Editor's character array le[]. */ void lineEditor() { int L; switch(tb) { // max number of characters permitted case 0: L = L0[f0]; break; // in NAD field being edited case 5: L = L5[f5]; break; // in crops field being edited case 6: L = 8; // in transaction field being edited } /* current number of characters in the line editor array | maximum capacity of line editor array | | */ if(en < L) { // if there's still room in the field for more characters /* character beyond the ultimate character in the array | current cursor position | | stepping backwards towards the cursor | | | */ for(int i = en; i >= ec; i--) // shift all chars from cursor one place le[i + 1] = le[i]; // right to make room for new character en++; // then increment length of field content } if(ec < L) { // if the cursor has not yet reached the end of the field le[ec] = ka; // put the new character at cursor position /* current position of cursor | total number of characters in the line editor field | | */ if(ec >= en) { // if currently at end of textual content le[ec + 1] = (char)0; // make character beyond into a NULL char en++; // increment number of characters of content } /* current cursor position | max number of character spaces in the line editor field | | advance the cursor one position right | | | */ if(ec < L) ec++; showLE(); // display textual content of le[] array } } /* THE KEYBOARD EVENTS HANDLER: A key press event generates two values: a keyboard Scan Code and an ASCII value. All key-press events generate a Scan Code but an ASCII value is given only for printable characters. For each key-press event, a Scan Code and an ASCII value [where applicable] are put in the global variables ks and ka, after which main() calls this function to act upon the values presented. Called from 1 place in main().*/ void KEH() { if(ea == 0 || tb == 6) // if Line Editor not active or we're in Tab 6 switch(ks) { // These keys are used outside the Line Editor case 81: // PgUp on number pad case 112: PgUpDn(-10); return; // Page-Up [10 lines to the page] case 89: // PgDn on number pad case 117: PgUpDn(+10); return; // Page-Down case 88: // down-arrow on number pad case 116: MWdn(); return; // down-arrow case 80: // up-arrow on number pad case 111: MWup(); return; // up-arrow } // DEAL WITH NON-PRINTABLE CONTROL CHARACTERS IN LINE EDITOR switch(ks) { // KEYBOARD SCAN CODES SWITCH case 50: return; // block the shift code case 113: leftArrow(); return; // if LEFTWARDS arrow key pressed case 114: rightArrow(); return; // if RIGHTWARDS-arrow key pressed case 22: bkSp(); return; // if 'Backspace' key pressed case 119: Del(); return; // if 'Delete' key pressed case 36: CR(); return; // carriage return case 9: Esc(); return; // if 'Escape' key pressed. case 110: homeEnd(0); return; // 'Home' key case 115: homeEnd(en); return; // 'End' key } if(ka < 32 || ka > 127) return; // non-printable ASCII character lineEditor(); } // OPEN ALL DATA FILES IN READ-UPDATE MODE. Called only from main(). void openFiles() { FA = fopen("data/nad.dat","r+"); // open the main name & address file fseek(FA,32,SEEK_SET); // go to byte 32 of this file N0 = fgetc(FA); // Byte 32: number of occupied records H0 = fgetc(FA); // Byte 33: highest occupied record V0 = fgetc(FA); // Byte 34: dummy 'people alert' flag W0 = fgetc(FA); // Byte 35: LIST 'index re-sort needed' flags FB = fopen("data/diary.dat","r+"); // open the diary file N8 = fgetc(FB); // Byte 0: current number of items in DIARY H8 = fgetc(FA); // Byte 1: dummy: get highest occupied record V8 = fgetc(FB); // Byte 2: 'diary alert' flags W8 = fgetc(FA); // Byte 3: DIARY 'index re-sort needed' flags FC = fopen("data/crop.dat","r+"); // open the crops data file fseek(FC,32,SEEK_SET); // go to byte 32 of this file N5 = fgetc(FC); // Byte 32: number of occupied records H5 = fgetc(FC); // Byte 33: highest occupied record V5 = fgetc(FB); // Byte 34: setting of 'crops alert' flag W5 = fgetc(FB); // Byte 35: crop 'index re-sort needed' flags FD = fopen("data/fin.dat","r+"); // open the financial transactions file N6 = fgetc(FD); // Byte 0: number of transactions on file H6 = fgetc(FD); // Byte 1: highest occupied invoice record V6 = fgetc(FD); // Byte 2: state of 'payment alert' flags W6 = fgetc(FD); // Byte 3: trans 'index re-sort needed' bits T1getLand(1); // to prime the local longitude } // MAIN FUNCTION FROM WHICH ALL THE ABOVE APPLICATION FUNCTIONS ARE CALLED int main() { XEvent e; // reference to any event from the created window int s, // number of the current default screen mouseB; // number of the mouse button that was pressed 0, 1, 2, 3, 4 /* Check that you can actually open the X-display by getting a non-NULL reference to a display. Else send an error message to the terminal. */ XD = XOpenDisplay(NULL); if(XD == NULL) { printf("Cannot open display\n"); exit(1); } s = DefaultScreen(XD); // get the number of the default screen XG = DefaultGC(XD,s); // set the graphics context to use XW = XCreateSimpleWindow( // create an application window XD, // within the default display RootWindow(XD,s), // within the root window [desktop] 10,10, // position of top right corner 500,400, // width and height of the window 1, // border width in pixels BlackPixel(XD,s), // colour of window's border BlackPixel(XD,s) // colour of window's background ); // set the window title XStoreName(XD,XW,"FRACTAL ECONOMY PROGRAM by R J Morton YE572246C"); /* Make input possible from the keyboard, window controls and mouse then display the new window on the screen. Not all these bit masks are needed so comment out those that are not needed. */ XSelectInput(XD,XW, ExposureMask | ButtonPressMask | PointerMotionMask | KeyPressMask //| KeyReleaseMask | ButtonReleaseMask | StructureNotifyMask ); XMapWindow(XD,XW); // map the window to the display XFlush(XD); // flush the display events buffer /* Define a unique identifier WM_DELETE_WINDOW (referred to as an atom) that will invoke the X11 protocol that closes windows upon the display 'XD' we have just created. [At least I think this is what it means] */ Atom WM_DELETE_WINDOW = XInternAtom(XD,"WM_DELETE_WINDOW",False); /* Make 'delete window' events from this program's window visible as a ClientMessage event. */ XSetWMProtocols(XD,XW,&WM_DELETE_WINDOW,1); openFiles(); // open all application data files in read + update mode /* The event-handling [or 'run'] loop [a permanent loop] broken only by the 'break' function in response to an external event. */ while (1) { if(XPending(XD) > 0) { // Provided there are events on the event queue, XNextEvent(XD,&e); // go fetch the first event. if(e.type == Expose) // if an 'expose' event has occurred showTab(); // draw content of the tab window else if(e.type == ButtonPress) { // IF A MOUSE BUTTON CLICKED: if(e.xbutton.button == Button1) { // if left mouse button clicked mx = e.xbutton.x; // set x-coordinate of click my = e.xbutton.y; // set y-coordinate of click MEH(); // call Mouse Event Handler } else if(e.xbutton.button == Button4) { // mouse wheel up-button clicked mx = e.xbutton.x; // set x-coordinate of click my = e.xbutton.y; // set y-coordinate of click MWup(); // mouse wheel scroll up } else if(e.xbutton.button == Button5) { // mouse wheel down-button clicked mx = e.xbutton.x; // set x-coordinate of click my = e.xbutton.y; // set y-coordinate of click MWdn(); // mouse wheel scroll down } } else if(e.type == KeyPress) { // IF A KEY HAS BEEN PRESSED: ks = e.xkey.keycode; // keyboard scan code of pressed key ka = (int)XkbKeycodeToKeysym ( // ASCII code of the scanned key XD, // window display pointer ks, // key scan [position] code 0, // key level e.xkey.state & ShiftMask ? 1 : 0 // upper or lower case ); KEH(); // Keyboard Event Handler [KEH] } /* Else if a box control '- + x' has been clicked, break out of the permanent while() loop, close the window and exit. Should also check here for other client message types; however, as the only protocol registered above is WM_DELETE_WINDOW, it is safe for this program. */ else if (e.type == ClientMessage) break; } else { T3sunTime(0); // call the 1-second service function usleep(50000); // sleep for 50 milliseconds } } // end of outer permanent while() loop XCloseDisplay(XD); // clear this window from the X11 display 'XD' fclose(FA); // close the name and address file 'nad.dat' fclose(FB); // close the 'diary.dat' fseek(FC,34,SEEK_SET); // seek start of flag data in 'crop.dat' fputc(V5,FC); // save the crop alert flag fclose(FC); // close the crops file 'crop.dat' fseek(FD,2,SEEK_SET); // seek start of flag data in 'fin.dat' fputc(V6,FD); // save the invoice alert flag fclose(FD); // close the financial transactions file 'fin.dat' return 0; // return that everything went OK } // end of main() : END OF PROGRAM : R J Morton