DECLARE FUNCTION ATAN2F! (y!, x!) DECLARE SUB BoldBox (TopRow%, BottomRow%, LeftColumn%, RightColumn%) DECLARE SUB ChangeHot (Hot%, i%, SkipLines%, ShortColumn%, Short$(), LongColumn%, Long$()) DECLARE SUB FaintBox (TopRow%, BottomRow%, LeftColumn%, RightColumn%) DECLARE SUB GChangeHot (Hot%, i%, SkipLines%, ShortColumn%, Short$(), LongColumn%, Long$()) DECLARE SUB Shadow (Cast%, TopRow%, BottomRow%, LeftColumn%, RightColumn%) COMMON SHARED FALSE%, TRUE%, BestMode%, Foreground%, Background%, CDstr$, CSstr$, DSstr$, OPstr$ FUNCTION ATAN2F (y!, x!) 'arc-tangent function with two arguments, returning result from -Pi to +Pi. IF (y! = 0!) AND (x! = 0!) THEN 'answer technically undefined; just return 0. ATAN2F = 0! ELSE IF x! > 0! THEN 'ordinary ATN function will work fine ATAN2F = ATN(y! / x!) ELSEIF x! = 0! THEN IF y! > 0! THEN ATAN2F = 1.570795 ELSE ATAN2F = -1.570795 END IF ELSE 'x < 0 Prince! = ATN(y! / x!) IF Prince! > 0! THEN ATAN2F = Prince! - 3.141591 ELSE ATAN2F = Prince! + 3.141591 END IF END IF END IF END FUNCTION SUB Blanker (R1%, R2%, C1%, C2%) IF BestMode% > 2 THEN COLOR Foreground%, Background% FOR i% = R1% TO R2% LOCATE i%, C1% FOR j% = C1% TO C2% PRINT " "; NEXT j% NEXT i% END SUB SUB BoldBox (TopRow%, BottomRow%, LeftColumn%, RightColumn%) 'draw a bold-outlined box 3 rows high (1 row inside) on a text screen. FOR i% = (LeftColumn% + 1) TO (RightColumn% - 1) LOCATE TopRow%, i%: PRINT CHR$(205); NEXT i% LOCATE TopRow%, RightColumn%: PRINT CHR$(187); FOR i% = (TopRow% + 1) TO (BottomRow% - 1) LOCATE i%, RightColumn%: PRINT CHR$(186); NEXT i% LOCATE BottomRow%, RightColumn%: PRINT CHR$(188); FOR i% = (RightColumn% - 1) TO (LeftColumn% + 1) STEP -1 LOCATE BottomRow%, i%: PRINT CHR$(205); NEXT i% LOCATE BottomRow%, LeftColumn%: PRINT CHR$(200); FOR i% = (BottomRow% - 1) TO (TopRow% + 1) STEP -1 LOCATE i%, LeftColumn%: PRINT CHR$(186); NEXT i% LOCATE TopRow%, LeftColumn%: PRINT CHR$(201); END SUB SUB ChangeHot (Hot%, i%, SkipLines%, ShortColumn%, Short$(), LongColumn%, Long$()) OldHot% = Hot% Hot% = i% COLOR Foreground%, Background% ColdRow% = OldHot% * (3 + SkipLines%) + 3 LOCATE ColdRow%, ShortColumn% PRINT Short$(OldHot%); HotRow% = Hot% * (3 + SkipLines%) + 3 LOCATE HotRow%, LongColumn% PRINT Long$(Hot%); LOCATE HotRow%, ShortColumn% COLOR Background%, Foreground% PRINT Short$(Hot%); COLOR Foreground%, Background% END SUB SUB FaintBox (TopRow%, BottomRow%, LeftColumn%, RightColumn%) 'draw a faint-outlined box 3 rows high (1 row inside) on a text screen. FOR i% = (LeftColumn% + 1) TO (RightColumn% - 1) LOCATE TopRow%, i%: PRINT CHR$(196); NEXT i% LOCATE TopRow%, RightColumn%: PRINT CHR$(191); FOR i% = (TopRow% + 1) TO (BottomRow% - 1) LOCATE i%, RightColumn%: PRINT CHR$(179); NEXT i% LOCATE BottomRow%, RightColumn%: PRINT CHR$(217); FOR i% = (RightColumn% - 1) TO (LeftColumn% + 1) STEP -1 LOCATE BottomRow%, i%: PRINT CHR$(196); NEXT i% LOCATE BottomRow%, LeftColumn%: PRINT CHR$(192); FOR i% = (BottomRow% - 1) TO (TopRow% + 1) STEP -1 LOCATE i%, LeftColumn%: PRINT CHR$(179); NEXT i% LOCATE TopRow%, LeftColumn%: PRINT CHR$(218); END SUB SUB GChangeHot (Hot%, i%, SkipLines%, ShortColumn%, Short$(), LongColumn%, Long$()) 'variant of ChangeHot for graphics screens, with no reverse text available. OldHot% = Hot% Hot% = i% Col1% = ShortColumn% - 1 Col2% = ShortColumn% + LEN(Short$(1)) ColdRow% = OldHot% * (4 + SkipLines%) - 1 TopRow% = ColdRow% - 1 BottomRow% = ColdRow% + 1 CALL Shadow(0, TopRow%, BottomRow%, Col1%, Col2%) HotRow% = Hot% * (4 + SkipLines%) - 1 TopRow% = HotRow% - 1 BottomRow% = HotRow% + 1 CALL Shadow(1, TopRow%, BottomRow%, Col1%, Col2%) END SUB SUB GetFileName (Text$, LineN%, NewName$) GetFileN: COLOR Foreground%, Background%: LOCATE LineN%, 1: PRINT Text$; LOCATE LineN%, LEN(Text$) + 1: COLOR Background%, Foreground%: PRINT " "; LOCATE LineN%, LEN(Text$) + 1: INPUT ; "", FileN$ IF LEN(FileN$) < 1 OR LEN(FileN$) > 8 THEN GOTO GetFileN NewName$ = "" FOR i% = 1 TO LEN(FileN$) T$ = MID$(FileN$, i%, 1) T% = ASC(T$) OK1% = (T% = 33) OR ((T% >= 35) AND (T% <= 41)) '!#$%&() OK2% = (T% = 44) OR (T% = 45) ',- OK3% = (T% >= 48) AND (T% <= 57) '0123456789 OK4% = (T% >= 64) AND (T% <= 90) '@ABC...XYZ OK5% = (T% >= 97) AND (T% <= 123) 'abc...xyz{ OK6% = (T% = 125) OR (T% = 126) '}~ OK% = OK1% OR OK2% OR OK3% OR OK4% OR OK5% OR OK6% IF OK5% THEN T$ = UCASE$(T$) IF OK% THEN NewName$ = NewName$ + T$ ELSE NewName$ = NewName$ + "_" NEXT i% LOCATE LineN%, LEN(Text$) + 1: PRINT " "; LOCATE LineN%, LEN(Text$) + 1: PRINT NewName$; WaitTime! = 1!: StartTime! = TIMER: DO: TimeNow! = TIMER: LOOP UNTIL (TimeNow! - StartTime!) > WaitTime! COLOR Foreground%, Background% END SUB SUB GRUNMENU (LastL%, Number%, Short$(), Initial$(), Long$(), SLen%, LLen%, OK%(), Hot%) 'version of RUNMENU modified for graphics screens, with no reverse text available. 'move highlight on a menu screen which has already been created. FreeLines% = LastL% - 2 - 4 * Number% SkipLines% = CINT(FreeLines% / CSNG(Number% + 1)) FreeColumns% = 80 - (SLen% + 2) - LLen% SkipColumns% = FreeColumns% \ 3 ShortColumn% = SkipColumns% + 2 LongColumn% = ShortColumn% + SLen% + SkipColumns% + 1 Enter$ = CHR$(13) GAwaitKey: a$ = INKEY$: IF LEN(a$) = 0 THEN GOTO GAwaitKey Done% = FALSE% IF a$ = Enter$ THEN Done% = TRUE% ELSEIF ((LEN(a$) = 2) AND (RIGHT$(a$, 1) = "P")) THEN 'Down arrow key i% = Hot% GTryDown: i% = i% + 1 IF i% > Number% THEN i% = 1 IF i% <> Hot% THEN IF OK%(i%) THEN CALL GChangeHot(Hot%, i%, SkipLines%, ShortColumn%, Short$(), LongColumn%, Long$()) ELSE GOTO GTryDown END IF ELSE BEEP FOR j% = 1 TO Number% IF j% <> Hot% THEN TrialRow% = j% * (4 + SkipLines%) - 1 LOCATE TrialRow%, LongColumn% PRINT " NOT AVAILABLE AT THIS TIME "; END IF NEXT j% END IF ELSEIF ((LEN(a$) = 2) AND (RIGHT$(a$, 1) = "H")) THEN 'Up arrow key i% = Hot% GTryUp: i% = i% - 1 IF i% = 0 THEN i% = Number% IF i% <> Hot% THEN IF OK%(i%) THEN CALL GChangeHot(Hot%, i%, SkipLines%, ShortColumn%, Short$(), LongColumn%, Long$()) ELSE GOTO GTryUp END IF ELSE BEEP FOR j% = 1 TO Number% IF j% <> Hot% THEN TrialRow% = j% * (4 + SkipLines%) - 1 LOCATE TrialRow%, LongColumn% PRINT " NOT AVAILABLE AT THIS TIME "; END IF NEXT j% END IF ELSE a$ = UCASE$(a$) FOR i% = 1 TO Number% IF a$ = Initial$(i%) THEN IF OK%(i%) THEN CALL GChangeHot(Hot%, i%, SkipLines%, ShortColumn%, Short$(), LongColumn%, Long$()) ELSE BEEP TrialRow% = i% * (4 + SkipLines%) - 1 LOCATE TrialRow%, LongColumn% + 1 PRINT "NOT AVAILABLE AT THIS TIME "; END IF EXIT FOR END IF NEXT i% END IF IF (NOT Done%) GOTO GAwaitKey END SUB SUB GSETMENU (LastL%, Number%, Short$(), Initial$(), Long$(), SLen%, LLen%, OK%(), Hot%) 'variant of SETMENU used with graphics screens, where reverse text is not available. 'draw all of the boxes on a menu screen which has already been titled. FreeLines% = LastL% - 2 - 4 * Number% SkipLines% = CINT(FreeLines% / CSNG(Number% + 1)) FreeColumns% = 80 - (SLen% + 2) - LLen% SkipColumns% = FreeColumns% \ 3 LeftColumn% = SkipColumns% + 1 RightColumn% = LeftColumn% + SLen% + 1 FOR i% = 1 TO Number% TopRow% = i% * (4 + SkipLines%) - 2 BottomRow% = TopRow% + 2 IF OK%(i%) THEN CALL BoldBox(TopRow%, BottomRow%, LeftColumn%, RightColumn%) LOCATE TopRow% + 1, LeftColumn% + 1 PRINT Short$(i%); IF i% = Hot% THEN CALL Shadow(1, TopRow%, BottomRow%, LeftColumn%, RightColumn%) END IF LOCATE TopRow% + 1, RightColumn% + SkipColumns% + 1 PRINT Long$(i%); ELSE CALL FaintBox(TopRow%, BottomRow%, LeftColumn%, RightColumn%) LOCATE TopRow% + 1, LeftColumn% + 1 PRINT Short$(i%); END IF NEXT i% END SUB SUB OpenCOM (LineOpen%, PN%, Baud$, Parity$, DataBits$, StopBits$, StartCol%, UseRow%) IF LineOpen% THEN CLOSE #1 LineOpen% = FALSE% END IF PN$ = RIGHT$(STR$(PN%), 1) MakeString: ON LOCAL ERROR GOTO 0 'in case we are looping back; avoid recursion OpenString$ = "COM" + PN$ + ": " + Baud$ + "," + Parity$ + "," + DataBits$ + "," + StopBits$ OpenString$ = OpenString$ + ",BIN,RB32767" + CDstr$ + CSstr$ + DSstr$ + OPstr$ ON LOCAL ERROR GOTO Relax OPEN OpenString$ FOR INPUT AS #1 ' 'delay for at least 1 second for lines to stabilize, but WITHOUT using the 'timer functions (clock interrupts) that interfere with the process! LOCATE UseRow%, StartCol% PRINT " ..... Please Wait "; FOR jPlace% = 0 TO 19 FOR k99% = 1 TO 99 LOCATE UseRow%, StartCol% + jPlace% PRINT CHR$(32); LOCATE UseRow%, StartCol% + jPlace% PRINT CHR$(177); NEXT k99% NEXT jPlace% ' LineOpen% = TRUE% ' ON LOCAL ERROR GOTO 0 'disable special handler below ' 'read in and thereby dispose of any trash left in buffer, due to 'an apparent bug in BASIC, or perhaps in my hardware: IF LOC(1) THEN b$ = INPUT$(LOC(1), #1) ' PLAY "MB L64 o4 C D E F G A B o5 C" 'quick scale to mark successful open LOCATE UseRow%, StartCol% PRINT " "; EXIT SUB ' Relax: 'allow for case of connection through defective wiring IF ERDEV = 128 THEN CSstr$ = ",CS0" BEEP CLS LOCATE 6, 23 PRINT "POSSIBLE COMMUNICATIONS PROBLEM:" LOCATE 8, 10 PRINT "Clear To Send (CTS) line voltage did not appear in 0.3 seconds." LOCATE 10, 10 PRINT "Opening of communications will be attempted without use of CTS." LOCATE 21, 35 PRINT "Press any key and wait for retry..." DO UNTIL INKEY$ <> "": LOOP RESUME MakeString ELSEIF ERDEV = 129 THEN DSstr$ = ",DS0" BEEP CLS LOCATE 6, 23 PRINT "POSSIBLE COMMUNICATIONS PROBLEM:" LOCATE 8, 10 PRINT "Data Set Ready (DSR) line voltage did not appear in 0.3 seconds." LOCATE 10, 10 PRINT "Opening of communications will be attempted without use of DSR." LOCATE 21, 35 PRINT "Press any key and wait for retry..." DO UNTIL INKEY$ <> "": LOOP RESUME MakeString ELSEIF ERDEV = 130 THEN CDstr$ = ",CD0" BEEP CLS LOCATE 6, 23 PRINT "POSSIBLE COMMUNICATIONS PROBLEM:" LOCATE 8, 10 PRINT "Data Carrier Detect (DCD) line voltage did not appear in 0.3 seconds." LOCATE 10, 10 PRINT "Opening of communications will be attempted without use of DCD." LOCATE 21, 35 PRINT "Press any key and wait for retry..." DO UNTIL INKEY$ <> "": LOOP RESUME MakeString ELSE BEEP CLS LOCATE 6, 25 PRINT "FATAL COMMUNICATIONS PROBLEM:" LOCATE 8, 10 PRINT "Basic's ERDEV function returned "; ERDEV LOCATE 10, 10 PRINT "Communication to the digitiser could not be established." LOCATE 12, 10 PRINT "Check continuity of wiring, power to digitiser, and selection" LOCATE 14, 10 PRINT "of COM1/COM2 in the Setup/Manual menu." LOCATE 21, 45 PRINT "Press any key ..." DO UNTIL INKEY$ <> "": LOOP RESUME NEXT END IF END SUB SUB RUNMENU (LastL%, Number%, Short$(), Initial$(), Long$(), SLen%, LLen%, OK%(), Hot%) 'move highlight on a menu screen which has already been created. FreeLines% = LastL% - 4 - 3 * Number% SkipLines% = CINT(FreeLines% / CSNG(Number% + 1)) FreeColumns% = 80 - (SLen% + 2) - LLen% SkipColumns% = FreeColumns% \ 3 ShortColumn% = SkipColumns% + 2 LongColumn% = ShortColumn% + SLen% + SkipColumns% + 1 Enter$ = CHR$(13) AwaitKey: a$ = INKEY$: IF LEN(a$) = 0 THEN GOTO AwaitKey Done% = FALSE% IF a$ = Enter$ THEN Done% = TRUE% ELSEIF ((LEN(a$) = 2) AND (RIGHT$(a$, 1) = "P")) THEN 'Down arrow key i% = Hot% TryDown: i% = i% + 1 IF i% > Number% THEN i% = 1 IF i% <> Hot% THEN IF OK%(i%) THEN CALL ChangeHot(Hot%, i%, SkipLines%, ShortColumn%, Short$(), LongColumn%, Long$()) ELSE GOTO TryDown END IF ELSE BEEP FOR j% = 1 TO Number% IF j% <> Hot% THEN TrialRow% = j% * (3 + SkipLines%) + 3 LOCATE TrialRow%, LongColumn% PRINT " NOT AVAILABLE AT THIS TIME "; END IF NEXT j% END IF ELSEIF ((LEN(a$) = 2) AND (RIGHT$(a$, 1) = "H")) THEN 'Up arrow key i% = Hot% TryUp: i% = i% - 1 IF i% = 0 THEN i% = Number% IF i% <> Hot% THEN IF OK%(i%) THEN CALL ChangeHot(Hot%, i%, SkipLines%, ShortColumn%, Short$(), LongColumn%, Long$()) ELSE GOTO TryUp END IF ELSE BEEP FOR j% = 1 TO Number% IF j% <> Hot% THEN TrialRow% = j% * (3 + SkipLines%) + 3 LOCATE TrialRow%, LongColumn% PRINT " NOT AVAILABLE AT THIS TIME "; END IF NEXT j% END IF ELSE a$ = UCASE$(a$) FOR i% = 1 TO Number% IF a$ = Initial$(i%) THEN IF OK%(i%) THEN CALL ChangeHot(Hot%, i%, SkipLines%, ShortColumn%, Short$(), LongColumn%, Long$()) ELSE BEEP TrialRow% = i% * (3 + SkipLines%) + 3 LOCATE TrialRow%, LongColumn% + 1 PRINT "NOT AVAILABLE AT THIS TIME "; END IF EXIT FOR END IF NEXT i% END IF IF (NOT Done%) GOTO AwaitKey END SUB SUB SETMENU (LastL%, Number%, Short$(), Initial$(), Long$(), SLen%, LLen%, OK%(), Hot%) 'draw all of the boxes on a menu screen which has already been titled. FreeLines% = LastL% - 4 - 3 * Number% SkipLines% = CINT(FreeLines% / CSNG(Number% + 1)) FreeColumns% = 80 - (SLen% + 2) - LLen% SkipColumns% = FreeColumns% \ 3 LeftColumn% = SkipColumns% + 1 RightColumn% = LeftColumn% + SLen% + 1 FOR i% = 1 TO Number% TopRow% = i% * (3 + SkipLines%) + 2 BottomRow% = TopRow% + 2 IF OK%(i%) THEN CALL BoldBox(TopRow%, BottomRow%, LeftColumn%, RightColumn%) LOCATE TopRow% + 1, LeftColumn% + 1 IF i% = Hot% THEN COLOR Background%, Foreground% PRINT Short$(i%); COLOR Foreground%, Background% ELSE PRINT Short$(i%); END IF LOCATE TopRow% + 1, RightColumn% + SkipColumns% + 1 PRINT Long$(i%); ELSE CALL FaintBox(TopRow%, BottomRow%, LeftColumn%, RightColumn%) LOCATE TopRow% + 1, LeftColumn% + 1 PRINT Short$(i%); END IF NEXT i% END SUB SUB SetProgPath (Outs, DefaultDrive$, CD$, ProgPath$) ' 'Determine default drive, and locate directory containing this program. ' CLS COLOR Foreground%, Background% LOCATE 12, 30 PRINT "One moment, please ..." ' 'Default Disk Drive ' DefaultDrive$ = LEFT$(CURDIR$, 2) ' 'Determine current directory, and check if this program is in it. ' CD$ = UCASE$(CURDIR$) IF RIGHT$(CD$, 1) = "\" THEN CD$ = LEFT$(CD$, LEN(CD$) - 1) 'look for this program in current directory: Trial$ = CD$ + "\DIGITISE.*" IF LEN(DIR$(Trial$)) THEN ProgPath$ = CD$ ELSE ' 'If program is not in current directory, then ' get PATH and use it to locate subdirectory with program in it. ' Path$ = ENVIRON$("PATH") IF Path$ = "" THEN Path$ = DefaultDrive$ IF RIGHT$(Path$, 1) <> ";" THEN Path$ = Path$ + ";" StartLook% = 1 DO Division% = INSTR(StartLook%, Path$, ";") IF Division% = 0 THEN EXIT DO LOCATE 16, 5 PRINT "Small problem: Location of this program (DIGITISE.*) could not " LOCATE 17, 5 PRINT "be found in current directory (CD), or in PATH." LOCATE 18, 5 PRINT "Therefore, any setup (.MOD) files stored with it will be inaccessible," LOCATE 19, 5 PRINT "and only setup files in the current directory can be accessed." LOCATE 20, 5 PRINT "Possible solutions: Switch CD to directory containing these files, or" LOCATE 21, 5 PRINT "put the directory containing DIGITISE.* into the PATH." LOCATE 23, 41 PRINT "Press any key to continue ...." DO UNTIL INKEY$ <> "": LOOP ProgPath$ = CD$ ELSE Trial$ = MID$(Path$, StartLook%, Division% - StartLook%) IF RIGHT$(Trial$, 1) = "\" THEN Trial$ = LEFT$(Trial$, LEN(Trial$) - 1) Fil$ = Trial$ + "\DIGITISE.*" IF LEN(DIR$(Fil$)) THEN ProgPath$ = Trial$ EXIT DO ELSE StartLook% = Division% + 1 END IF END IF LOOP END IF ' END SUB SUB Shadow (Cast%, TopRow%, BottomRow%, LeftColumn%, RightColumn%) 'puts a shadow below/to right of highlighted box FOR i% = (TopRow% + 1) TO (BottomRow% + 1) LOCATE i%, RightColumn% + 1 IF Cast% THEN PRINT CHR$(177); ELSE PRINT CHR$(0); END IF NEXT i% FOR i% = (RightColumn% + 1) TO (LeftColumn% + 1) STEP -1 LOCATE BottomRow% + 1, i% IF Cast% THEN PRINT CHR$(177); ELSE PRINT CHR$(0); END IF NEXT i% END SUB SUB TitleScreen 'Opening title screen to introduce program '(get Foreground%, Background% from SHARED COMMON) SCREEN 0, , 0, 0 'text mode CLS COLOR Foreground%, Background% LOCATE 1, 1 PRINT CHR$(201); : FOR K% = 1 TO 78: PRINT CHR$(205); : NEXT K%: PRINT CHR$(187) PRINT CHR$(186); " y "; CHR$(186) PRINT CHR$(186); " "; CHR$(186) PRINT CHR$(186); " "; CHR$(179); " "; CHR$(186) PRINT CHR$(186); " "; CHR$(179); " DIGITISE "; CHR$(186) PRINT CHR$(186); " "; CHR$(179); " "; CHR$(186) PRINT CHR$(186); " "; CHR$(192); PRINT CHR$(196); CHR$(196); CHR$(196); CHR$(196); CHR$(196); CHR$(196); PRINT CHR$(196); CHR$(196); CHR$(196); CHR$(196); CHR$(196); CHR$(196); PRINT " x "; CHR$(186) PRINT CHR$(186); " "; CHR$(186) PRINT CHR$(186); " A program for converting x/y digitiser input to ASCII disk files. "; CHR$(186) PRINT CHR$(186); " (C) by Peter Bird "; CHR$(186) PRINT CHR$(186); " UCLA "; CHR$(186) PRINT CHR$(186); " August, 1996 "; CHR$(186) PRINT CHR$(186); " Features: "; CHR$(186) PRINT CHR$(186); " "; CHR$(251); " Easy hardware-interface configuration: interactive, or from file. "; CHR$(186) PRINT CHR$(186); " "; CHR$(251); " Optional 'squaring-up' of axes and application of scale factors. "; CHR$(186) PRINT CHR$(186); " "; CHR$(251); " Results displayed on-screen as you work. "; CHR$(186) PRINT CHR$(186); " "; CHR$(251); " An old ouput file may also be displayed for location reference. "; CHR$(186) PRINT CHR$(186); " "; CHR$(251); " Multiple line segments per file, and multiple files per session. "; CHR$(186) PRINT CHR$(186); " "; CHR$(251); " Optional comment lines in output file to identify the line segments."; CHR$(186) PRINT CHR$(186); " Limitations: "; CHR$(186) PRINT CHR$(186); " "; CHR$(4); " Communication is one-way; program does not control digitiser. "; CHR$(186) PRINT CHR$(186); " "; CHR$(4); " Input must come through a serial (COM) port, not a parallel port. "; CHR$(186) PRINT CHR$(200); : FOR K% = 1 TO 78: PRINT CHR$(205); : NEXT K%: PRINT CHR$(188) LOCATE 25, 26 COLOR Background%, Foreground% PRINT " Press any key to continue .... "; COLOR Foreground%, Background% DO UNTIL INKEY$ <> "": LOOP END SUB