CtInfoCommandImpl.mesa
Copyright Ó 1985, 1992 by Xerox Corporation. All rights reserved.
Bloomenthal, October 13, 1992 3:15 pm PDT
Heckbert, June 23, 1988 8:42:17 pm PDT
DIRECTORY AISFileFormat, AISIO, Basics, CommanderOps, Convert, CtBasic, CtDispatch, CtMap, CtViewer, FileNames, FS, ImagerSample, IO, PFS, PreDebug, Process, Rope, TiogaExtraOps, TiogaOps, TypeScript, ViewerClasses, ViewerIO, ViewerOps;
CtInfoCommandImpl: CEDAR PROGRAM
IMPORTS AISIO, Basics, CommanderOps, Convert, CtBasic, CtDispatch, CtMap, CtViewer, FileNames, FS, ImagerSample, IO, PFS, PreDebug, Process, Rope, TiogaExtraOps, TiogaOps, TypeScript, ViewerIO, ViewerOps
~ BEGIN
Types
SampleMaps:  TYPE ~ CtBasic.SampleMaps;
Box:    TYPE ~ CtBasic.Box;
RGB:    TYPE ~ CtBasic.RGB;
CtProc:   TYPE ~ CtDispatch.CtProc;
Cmap:    TYPE ~ CtMap.Cmap;
STREAM:   TYPE ~ IO.STREAM;
ROPE:    TYPE ~ Rope.ROPE;
Viewer:   TYPE ~ ViewerClasses.Viewer;
ViewerClassRec: TYPE ~ ViewerClasses.ViewerClassRec;
AIS Info
BytesForAISWords: PROC [aisWords: INT] RETURNS [INT] ~ INLINE {
RETURN[LONG[AISFileFormat.bytesPerAISWord]*aisWords];
};
ReadBytes: UNSAFE PROC [stream: STREAM, base: LONG POINTER, bytes: INT] ~ UNCHECKED {
actualBytes: INT ~ IO.UnsafeGetBlock[stream, [base: LOOPHOLE[base], count: bytes]];
IF actualBytes # bytes THEN ERROR IO.EndOfStream[stream]; -- file too short
};
AISError: ERROR = CODE;
PrintAIS: PROC [s: STREAM, fileName: ROPE, verbose: BOOL] ~ {
Paul Heckbert, 30 May 1988
ENABLE AISError => GOTO Bad;
PrintInfo: PROC [i: AISIO.Info] ~ {
V: PROC [c: AISFileFormat.Card16] RETURNS [CARD] ~ {RETURN[Basics.Card16FromH[c]]};
IO.PutFL[s, "%g:\n\t%g x %g x %g bits", LIST[
IO.rope[name], IO.int[V[i.raster.scanLength]],
IO.int[V[i.raster.scanCount]],
IO.int[V[i.uca.bitsPerSample]]]];
IF i.placement # NIL THEN IO.PutF[s, ", origin: (%g, %g)",
IO.int[V[i.placement.xLeft]], IO.int[V[i.placement.yBottom]]];
IF i.photometry # NIL THEN IO.PutF[s, ", photometry present"];
IO.PutRope[s, "\n"];
FOR l: LIST OF ROPE ¬ i.comments, l.rest WHILE l # NIL DO
IF NOT Rope.IsEmpty[l.first] THEN IO.PutF1[s, "\t\t%g\n", IO.rope[l.first]];
ENDLOOP;
IF verbose THEN {
IO.PutF1[s, "\traster offset: %g\n", IO.int[i.rasterOffset]];
IO.PutF1[s, "\traster direction: %g\n", IO.rope[SELECT V[i.raster.scanDirection] FROM
3 => "scan right towards bottom",
0, 8 => "scan right towards top",
ENDCASE => "undefined"]];
IO.PutF1[s, "\t%g sample(s) per pixel\n", IO.int[V[i.raster.samplesPerPixel]]];
IO.PutF1[s, "\tcoding type: %g\n", IO.rope[SELECT i.raster.codingType FROM
nil => "nil", uca => "uca", ENDCASE => "undefined"]];
IO.PutF1[s, "\t%g wordsPerScanLine\n", IO.int[V[i.uca.wordsPerScanLine]]];
IO.PutF[s, "\t%g scanLinesPerBlock, %g paddingPerBlock\n",
IO.int[V[i.uca.scanLinesPerBlock]], IO.int[V[i.uca.paddingPerBlock]]];
};
};
info: AISIO.Info ¬ NIL;
name: ROPE ¬ FileNames.ResolveRelativePath[fileName];
info ¬ AISIO.ReadInfo[name !
FS.Error => {
IO.PutF[s, "Bad AIS file: %g\n(%g)\n", IO.rope[fileName], IO.rope[error.explanation]];
GOTO Bad;
};
AISIO.Error => CONTINUE
];
IF info # NIL
THEN PrintInfo[info]
ELSE { -- rolling our own
Int16:     TYPE ~ Basics.HWORD;
Card16:    TYPE ~ Basics.HWORD;
AttributeHeader: TYPE ~ AISFileFormat.AttributeHeader;
PartHeader:  TYPE ~ AISFileFormat.PartHeader;
RasterPart:  TYPE ~ AISFileFormat.RasterPart;
UCACoding:  TYPE ~ AISFileFormat.UCACoding;
PlacementPart: TYPE ~ AISFileFormat.PlacementPart;
PhotometryPart: TYPE ~ AISFileFormat.PhotometryPart;
CommentPart: TYPE ~ AISFileFormat.CommentPart;
IOrd: PROC [h: Int16] RETURNS [INT16] ~ {RETURN[Basics.Int16FromH[h]]};
COrd: PROC [h: Card16] RETURNS [CARD16] ~ {RETURN[Basics.Card16FromH[h]]};
Assert: PROC [title: ROPE, assertion: BOOL] ~ {
IF assertion THEN RETURN;
PrintInfo[info];
IO.PutF1[s, "\tAIS file structure is inconsistent (%g)\n", IO.rope[title]];
IO.Close[in];
AISError[];
};
in: IO.STREAM ~ PFS.StreamOpen[PFS.PathFromRope[name], read];
header: MACHINE DEPENDENT RECORD [
c: AttributeHeader, p: PACKED ARRAY [0..(BITS[WORD]-(BITS[AttributeHeader] MOD BITS[WORD])) MOD BITS[WORD]) OF [0..1]];
partHeader: MACHINE DEPENDENT RECORD [
c: PartHeader, p: PACKED ARRAY [0..(BITS[WORD]-(BITS[PartHeader] MOD BITS[WORD])) MOD BITS[WORD]) OF [0..1]];
raster: MACHINE DEPENDENT RECORD [
c: RasterPart, p: PACKED ARRAY [0..(BITS[WORD]-(BITS[RasterPart] MOD BITS[WORD])) MOD BITS[WORD]) OF [0..1]];
uca: MACHINE DEPENDENT RECORD [
c: UCACoding, p: PACKED ARRAY [0..(BITS[WORD]-(BITS[UCACoding] MOD BITS[WORD])) MOD BITS[WORD]) OF [0..1]];
placement: MACHINE DEPENDENT RECORD [
c: PlacementPart, p: PACKED ARRAY [0..(BITS[WORD]-(BITS[PlacementPart] MOD BITS[WORD])) MOD BITS[WORD]) OF [0..1]];
photometry: MACHINE DEPENDENT RECORD [
c: PhotometryPart, p: PACKED ARRAY [0..(BITS[WORD]-(BITS[PhotometryPart] MOD BITS[WORD])) MOD BITS[WORD]) OF [0..1]];
comment: MACHINE DEPENDENT RECORD [
c: CommentPart, p: PACKED ARRAY [0..(BITS[WORD]-(BITS[CommentPart] MOD BITS[WORD])) MOD BITS[WORD]) OF [0..1]];
info ¬ NEW[AISIO.InfoRep];
TRUSTED {ReadBytes[in, @header, AISFileFormat.byteSizeAttributeHeader]};
IF header.c.password # AISFileFormat.passwordValue THEN Assert["bad password", FALSE];
Assert["bad header size", COrd[header.c.length]>0 AND (COrd[header.c.length] MOD AISFileFormat.wordsPerAISPage)=0];
info.rasterOffset ¬ BytesForAISWords[COrd[header.c.length]];
FOR firstPart: BOOL ¬ TRUE, FALSE DO
startIndex, stopIndex: INT ¬ IO.GetIndex[in];
TRUSTED {ReadBytes[in, @partHeader, AISFileFormat.byteSizePartHeader]};
stopIndex ¬ startIndex+BytesForAISWords[256*partHeader.c.lengthHi+partHeader.c.lengthLo];
Assert["header bigger than rasterOffset", stopIndex <= info.rasterOffset];
SELECT partHeader.c.type FROM
nil => {
Assert["bad length for nil part", partHeader.c.lengthHi = 0 AND partHeader.c.lengthLo = 0];
EXIT; -- only correct way out
}; -- should be a zero word
raster => {
Assert["raster not first", firstPart]; -- raster part must be first
TRUSTED {ReadBytes[in, @raster, AISFileFormat.byteSizeRasterPart]};
Assert["part size too large", IO.GetIndex[in] <= stopIndex];
Assert["bad raster part size", COrd[raster.c.scanCount]>0 AND COrd[raster.c.scanLength]>0 AND COrd[raster.c.samplesPerPixel]>0];
SELECT raster.c.codingType FROM
uca => {
byteSizeCoding: INT ~ stopIndex-IO.GetIndex[in];
Assert["bad byteSizeCoding", byteSizeCoding <= AISFileFormat.byteSizeUCACoding];
TRUSTED {ReadBytes[in, @uca, byteSizeCoding]};
IF COrd[uca.c.bitsPerSample] = 0 THEN uca.c.bitsPerSample.lo ¬ 1; -- kludge
Assert["bad scans part", INT[COrd[uca.c.bitsPerSample]]*INT[COrd[raster.c.samplesPerPixel]]*INT[COrd[raster.c.scanLength]]<=INT[Basics.bitsPerByte]*BytesForAISWords[COrd[uca.c.wordsPerScanLine]]];
IF byteSizeCoding < AISFileFormat.byteSizeUCACoding THEN {
Assert["bad scanLinesPerBlock", uca.c.scanLinesPerBlock = AISFileFormat.nil];
uca.c.paddingPerBlock ¬ AISFileFormat.nil;
};
info.uca ¬ NEW[UCACoding ¬ uca.c];
};
ENDCASE => Assert["Unknown AIS coding type", FALSE];
info.raster ¬ NEW[RasterPart ¬ raster.c];
};
placement => {
TRUSTED {ReadBytes[in, @placement, AISFileFormat.byteSizePlacementPart]};
Assert["bad placement part size", IO.GetIndex[in] = stopIndex];
IF IOrd[placement.c.xLeft] = -1 AND IOrd[placement.c.yBottom] = -1 AND IOrd[placement.c.xWidth] = -1 AND IOrd[placement.c.yHeight] = -1
THEN NULL
ELSE Assert["bad place coordinates", IOrd[placement.c.xWidth]>0 AND IOrd[placement.c.yHeight]>0];
info.placement ¬ NEW[PlacementPart ¬ placement.c];
};
photometry => {
TRUSTED {ReadBytes[in, @photometry, AISFileFormat.byteSizePhotometryPart]};
Assert["bad photometry part size", IO.GetIndex[in] <= stopIndex];
IO.SetIndex[in, stopIndex]; -- Ignore histogram, if any
info.photometry ¬ NEW[PhotometryPart ¬ photometry.c];
};
comment => {
byteSizeComment: INT ~ stopIndex-IO.GetIndex[in];
length: NAT ¬ 0; -- number of characters in the string
Assert["bad comment part size", byteSizeComment IN [1..AISFileFormat.byteSizeCommentPart]];
TRUSTED {ReadBytes[in, @comment, byteSizeComment]};
length ¬ ORD[comment.c[0]];
Assert["bad comment part size", byteSizeComment >= INT[length+1]];
{ -- turn the comment into a ROPE
i: NAT ¬ 0;
p: PROC RETURNS [CHAR] ~ {RETURN[comment.c[i ¬ i+1]]};
rope: ROPE ¬ Rope.FromProc[len: length, p: p];
info.comments ¬ CONS[rope, info.comments];
};
};
ENDCASE => Assert["unknown part", TRUE];
IF IO.GetIndex[in] # stopIndex THEN Assert["header # sum of parts", FALSE];
ENDLOOP;
IO.Close[in];
PrintInfo[info];
};
EXITS Bad => NULL;
};
ctAISInfoUsage: ROPE ~ "Ct AISInfo <file(s)> [-verbose]: Print the AIS header info.";
CtAISInfo: CtProc ~ {
verbose: BOOL ¬ FALSE;
argv: CommanderOps.ArgumentVector ¬ CommanderOps.Parse[cmd];
IF argv.argc <= 1 THEN RETURN [error: ctAISInfoUsage];
FOR i: INT IN [1..argv.argc) DO
IF Rope.Equal[argv[i], "-verbose", FALSE] THEN verbose ¬ TRUE;
ENDLOOP;
FOR i: INT IN [1..argv.argc) DO
NameProc: PROC [file: ROPE] RETURNS [continue: BOOL ¬ TRUE] ~ {
Inner: PROC ~ {PrintAIS[cmd.out, file, verbose]};
Reject: PROC [reason: ROPE] RETURNS [reject: BOOL ¬ FALSE] ~ {
IO.PutF1[cmd.out, "%g\n", IO.rope[reason]];
};
once ¬ TRUE;
Process.CheckForAbort[];
[] ¬ PreDebug.Protect[Inner, Reject];
};
once: BOOL ¬ FALSE;
IF Rope.Equal[argv[i], "-verbose", FALSE] THEN LOOP;
IF Rope.Find[argv[i], "!"] = -1 THEN argv[i] ¬ Rope.Concat[argv[i], "!h"];
FS.EnumerateForNames[FileNames.ResolveRelativePath[argv[i]], NameProc
! FS.Error => GOTO Bad];
IF NOT once THEN IO.PutF1[cmd.out, "No such files: %g\n", IO.rope[argv[i]]];
ENDLOOP;
EXITS Bad => NULL;
};
Pixel Info Tool
Info:  TYPE ~ REF InfoRec;
InfoRec: TYPE ~ RECORD [ts, image: Viewer, out: STREAM, maps: SampleMaps, cmap: Cmap];
PixelInfo: TYPE ~ RECORD [valid: BOOL ¬ TRUE, pval: CARDINAL ¬ 0, rgb: RGB];
InfoTrack: CtViewer.MouseProc ~ {
info: Info ~ NARROW[clientData];
IF mouse.state = up THEN SELECT mouse.button FROM
left => APixel[info.out, info.maps, info.cmap, mouse.pos.x, mouse.pos.y];
middle => ManyPixels[info.out, info.maps, info.cmap, mouse.pos.x, mouse.pos.y, info.ts];
ENDCASE;
};
InfoDestroy: ViewerClasses.DestroyProc ~ {
CtViewer.UnregisterMouse[NARROW[self.data, Info].image, InfoTrack];
};
ctInfoUsage: ROPE ~ "Ct Info [x, y: INT]: Print the pixel value at (x, y).";
CtInfo: CtProc ~ {
ENABLE Convert.Error => GOTO ConvertError;
args: CommanderOps.ArgumentVector ¬ CommanderOps.Parse[cmd];
affect ¬ [[0, 0], [0, 0]];
SELECT args.argc FROM
1 => {
info: Info ¬ NEW[InfoRec];
TRUSTED {Process.Detach[FORK MakeViewer[info]]};
info.image ¬ viewer;
info.maps ¬ maps;
info.cmap ¬ CtMap.Read[];
CtViewer.RegisterMouse[info.image, InfoTrack, info];
};
3 => {
x: INTEGER ¬ Convert.IntFromRope[args[1]];
y: INTEGER ¬ Convert.IntFromRope[args[2]];
cmap: Cmap ¬ IF maps.bpp = 8 THEN CtMap.Read[] ELSE NIL;
IF NOT CoordinatesOK[maps, x, y] THEN RETURN[error:
IO.PutFR["(x, y) must be ([0..%g), [0..%g))", IO.int[maps.w], IO.int[maps.h]]];
APixel[cmd.out, maps, cmap, x, y];
};
ENDCASE => RETURN[error: "bad arguments"];
EXITS ConvertError => RETURN[error: "conversion error"];
};
MakeViewer: PROC [info: Info] ~ {
r: Rope.ROPE ~ "0 pt restIndent";
v: Viewer ¬ ViewerOps.CreateViewer[
flavor: $CtInfo,
info: [openHeight: 400, name: "Ct Info", data: info, column: right, iconic: TRUE]];
ViewerOps.OpenIcon[v];
info.ts ¬ TypeScript.Create[[parent: v, ww: v.cw, wh: v.ch, border: FALSE]];
TiogaExtraOps.PutProp[TiogaOps.LastWithin[TiogaOps.ViewerDoc[info.ts]], $Postfix, r];
info.out ¬ ViewerIO.CreateViewerStreams[NIL, info.ts].out;
};
APixel: PROC [out: STREAM, maps: SampleMaps, cmap: Cmap, x, y: NAT] ~ {
pix: PixelInfo ¬ GetPixelInfo[maps, cmap, x, y];
SELECT TRUE FROM
pix.valid = FALSE => IO.PutRope[out, "Pixel off screen\n"];
maps.bpp = 8 =>
IO.PutRope[out, Rope.Concat[
IO.PutFR["8 bit pixel at (%g, %g): %g ", IO.int[x], IO.int[y], IO.card[pix.pval]],
IO.PutFR["(r: %g, g: %g, b: %g)\n",
IO.int[cmap[0][pix.pval]], IO.int[cmap[1][pix.pval]], IO.int[cmap[2][pix.pval]]]]];
maps.bpp = 24 =>
IO.PutRope[out, IO.PutFLR["24 bit pixel at (%g, %g), r: %g, g: %g, b: %g\n", LIST[
IO.int[x], IO.int[y], IO.card[pix.rgb.r], IO.card[pix.rgb.g], IO.card[pix.rgb.b]]]];
ENDCASE;
};
ManyPixels: PROC [out: STREAM, maps: SampleMaps, cmap: Cmap, x, y: NAT, v: Viewer] ~ {
pixs: ARRAY [0..5) OF ARRAY [0..5) OF PixelInfo;
Bar: PROC ~ {
IF maps.bpp = 8
THEN IO.PutRope[out, "——————————————————————————————————\n"]
ELSE IO.PutRope[out, "——————————————————————\n"]
};
PrintLine: PROC [j: INTEGER, proc: PROC [i, j: INTEGER], reportY: BOOL ¬ FALSE] ~ {
IF proc # PrintX THEN IO.PutRope[out, "| "];
FOR i: NAT IN [0..5) DO proc[i, j]; ENDLOOP;
IF reportY THEN IO.PutF1[out, " %g", IO.int[y-2+j]];
IO.PutRope[out, "\n"];
};
PrintX: PROC [i, j: INTEGER] ~ {
IF maps.bpp = 8
THEN IO.PutF1[out, " %3g\t", IO.int[x-2+i]]
ELSE IO.PutF1[out, " %3g\t", IO.int[x-2+i]];
};
PrintPval: PROC [i, j: INTEGER] ~ {IO.PutF1[out, " %-3g\t\t|", IO.card[pixs[i][j].pval]]};
PrintRed: PROC [i, j: INTEGER] ~ {PrintCard["r", pixs[i][j].rgb.r]};
PrintGrn: PROC [i, j: INTEGER] ~ {PrintCard["g", pixs[i][j].rgb.g]};
PrintBlu: PROC [i, j: INTEGER] ~ {PrintCard["b", pixs[i][j].rgb.b]};
PrintCard: PROC [rope: Rope.ROPE, card: CARDINAL] ~ {
IF maps.bpp = 8
THEN IO.PutF[out, " %g: %3g\t|", IO.rope[rope], IO.card[card]]
ELSE IO.PutF1[out, " %3g\t|", IO.card[card]];
};
FOR j: NAT IN [0..5) DO
yy: INTEGER ¬ y-2+j;
FOR i: NAT IN [0..5) DO
pixs[i][j] ¬ GetPixelInfo[maps, cmap, x-2+i, yy];
ENDLOOP;
ENDLOOP;
IO.PutF[out, "\n%g bit pixels centered at (%g, %g):\n", IO.int[maps.bpp], IO.int[x], IO.int[y]];
Process.Pause[Process.MsecToTicks[100]];
[] ¬ v.class.scroll[v, thumb, 100];
PrintLine[0, PrintX];
Bar[];
FOR line: NAT IN [0..5) DO
IF maps.bpp = 8 THEN PrintLine[line, PrintPval];
PrintLine[line, PrintRed];
PrintLine[line, PrintGrn, TRUE];
PrintLine[line, PrintBlu];
Bar[];
ENDLOOP
};
CoordinatesOK: PROC [maps: SampleMaps, x, y: INTEGER] RETURNS [BOOL] ~ {
RETURN[x IN [0..maps.w) AND y IN [0..maps.h)];
};
GetPixelInfo: PROC [maps: SampleMaps, cmap: Cmap, x, y: INTEGER] RETURNS [p: PixelInfo] ~ {
IF CoordinatesOK[maps, x, y]
THEN SELECT maps.bpp FROM
8 => {
p.pval ¬ ImagerSample.Get[maps[0].map, [y, x]];
p.rgb ¬ [cmap[0][p.pval], cmap[1][p.pval], cmap[2][p.pval]];
};
24 => p.rgb ¬ CtBasic.GetRGBPixel[maps, x, y];
ENDCASE
ELSE p.valid ¬ FALSE;
};
Viewer Size Command
ctViewerSizeUsage: ROPE ~ "Ct ViewerSize: Print size of current ColorTrix viewer.";
CtViewerSize: CtProc ~ {
affect ¬ [[0, 0], [0, 0]];
IO.PutFL[cmd.out, "%g %g %g %g (xywh)\n",
LIST[IO.int[maps.x], IO.int[maps.y], IO.int[maps.w], IO.int[maps.h]]];
};
Bounding Box Command
ctBBoxUsage: ROPE ~ "Ct BBox [-w <xywh>]: Print non-background xywh.";
CtBBox: CtProc ~ {
box: Box ¬ BBox[maps];
affect ¬ [[0, 0], [0, 0]];
IO.PutFL[cmd.out, "%g %g %g %g\n", LIST[
IO.int[box.min.f], IO.int[box.min.s], IO.int[box.max.f-box.min.f], IO.int[box.max.s-box.min.s]]];
};
BBox: PROC [maps: SampleMaps] RETURNS [Box] ~ {
find the bounding box of the foreground (non-background) area of the maps
xmin: INT16 ¬ maps.x+maps.w;
ymin: INT16 ¬ maps.y+maps.h;
xmax: INT16 ¬ maps.x-1;
ymax: INT16 ¬ maps.y-1;
line: ImagerSample.SampleBuffer ~ ImagerSample.ObtainScratchSamples[maps.w];
bgbuf: ImagerSample.SampleBuffer ~ ImagerSample.ObtainScratchSamples[1];
bg: ImagerSample.Sample;
FOR chan: NAT IN [0..maps.nChannels) DO
read lower right pixel and regard that as the background color
ImagerSample.GetSamples[map: maps[chan].map, initIndex: [maps.y+maps.h-1, maps.x+maps.w-1], buffer: bgbuf, count: 1];
bg ¬ bgbuf[0];
FOR y: NAT IN [0..maps.h) DO
ImagerSample.GetSamples[map: maps[chan].map, initIndex: [maps.y+y, maps.x], buffer: line, count: maps.w];
FOR x: NAT IN [0..maps.w) DO
IF line[x] # bg THEN {
IF x < xmin THEN xmin ¬ x;
IF x > xmax THEN xmax ¬ x;
IF y < ymin THEN ymin ¬ y;
IF y > ymax THEN ymax ¬ y;
};
ENDLOOP;
ENDLOOP;
ENDLOOP;
ImagerSample.ReleaseScratchSamples[bgbuf];
ImagerSample.ReleaseScratchSamples[line];
RETURN[IF xmin <= xmax
THEN [[ymin, xmin], [ymax-ymin+1, xmax-xmin+1]]
ELSE [[0,0], [0,0]]];
};
Start Code
ViewerOps.RegisterViewerClass[$CtInfo, NEW[ViewerClassRec ¬ [destroy: InfoDestroy]]];
CtDispatch.RegisterCtOp["Information:", NIL,    NIL];
CtDispatch.RegisterCtOp["Info",    CtInfo,   ctInfoUsage, TRUE, FALSE];
CtDispatch.RegisterCtOp["AISInfo",   CtAISInfo,  ctAISInfoUsage, FALSE];
CtDispatch.RegisterCtOp["BBox",    CtBBox,   ctBBoxUsage, TRUE, FALSE];
CtDispatch.RegisterCtOp["ViewerSize",  CtViewerSize, ctViewerSizeUsage, TRUE, FALSE];
END.