DIRECTORY Commander USING [CommandProc, Register, Handle], CommandTool USING [ArgumentVector, Failed, Parse], Containers USING [Container, Create], Convert USING [RopeFromInt, IntFromRope], Icons USING [NewIconFromFile], IO USING [PutF, PutFR, int, STREAM], Imager USING [Context, MaskRectangle, SetFont, SetColor, SetXY, ShowRope, TranslateT, black, MakeGray], ImagerColor USING [ConstantColor], ImagerFont USING [Find, Font, RopeEscapement], Jukebox USING [bytesPerChirp, CloseJukebox, ArchiveCloseTune, Error, FindJukebox, Handle, OpenJukebox, OpenTune, Tune, TuneSize, EnergyRange, instances, singlePktLength, hangoverPackets, MissingChirp, EOF], Rope USING [Cat, ROPE, Equal, Length, Fetch, Substr], Vector2 USING [VEC], ViewerEvents USING [EventProc, RegisterEventProc], ViewerClasses USING [PaintProc, Viewer, ViewerClass, ViewerClassRec], ViewerOps USING [CreateViewer, RegisterViewerClass, SetOpenHeight], TuneAccess USING [EnergyBlock, GetEnergyProfile, ReadAmbientLevel]; TuneGraphsImpl: CEDAR PROGRAM IMPORTS Commander, CommandTool, Containers, Convert, Icons, IO, Imager, ImagerFont, Jukebox, Rope, ViewerEvents, ViewerOps, TuneAccess = { Context: TYPE ~ Imager.Context; VEC: TYPE ~ Vector2.VEC; Font: TYPE ~ ImagerFont.Font; CommandName: TYPE = {plotTune, plotDistribution}; Data: TYPE = REF DataRecord; DataRecord: TYPE = RECORD [ container: Containers.Container _ NIL, graphs: ViewerClasses.Viewer _ NIL, commandName: CommandName, jukeboxName: Rope.ROPE _ NIL, tuneString: Rope.ROPE _ NIL, height: INT, yMax: CARDINAL, rows: INT, width: INT, totalHeight: NAT, points: REF TwoDArrayOfPoints _ NEW [TwoDArrayOfPoints _ ALL[NIL]], helvetica6: Font _ ImagerFont.Find["Xerox/TiogaFonts/Helvetica6"], helvetica8: Font _ ImagerFont.Find["Xerox/TiogaFonts/Helvetica8"], helvetica10: Font _ ImagerFont.Find["Xerox/TiogaFonts/Helvetica10"], tuneNumberFont: Font]; leading: NAT = 2; separation: NAT = 10; buttonHeight: NAT = 15; headerHeight: NAT = 12; left: INT = 50; bottom: INT = leading+headerHeight+leading; top: INT = 10; maxWidth: INT = 500; gray: ImagerColor.ConstantColor = Imager.MakeGray[0.5]; maxRows: INT = 8; -- most that will fit on a screen [assumes a value of data.height - naughty] TwoDArrayOfPoints: TYPE = ARRAY [0..maxRows) OF REF ArrayOfPoints; RealInitialisedToZero: TYPE = REAL _ 0; ArrayOfPoints: TYPE = RECORD [ s: SEQUENCE max: NAT OF RealInitialisedToZero]; Title: PROC [context: Context, data: Data, t1, t2, t3: Rope.ROPE] ~ { Imager.SetFont[context, data.helvetica6]; Imager.SetXY[context, [-left+leading+leading, leading+headerHeight]]; Imager.ShowRope[context, t1]; Imager.SetFont[context, data.tuneNumberFont]; Imager.SetXY[context, [-left+leading+leading, leading]]; Imager.ShowRope[context, t2]; Imager.SetFont[context, data.helvetica6]; Imager.SetXY[context, [-left+leading+leading, 2*leading-headerHeight]]; Imager.ShowRope[context, t3]; }; BasicFrame: PROC [context: Context, data: Data, height: INT] ~ { Imager.SetFont[context, data.helvetica8]; Imager.MaskRectangle[context, [x: -1, y: 0, w: data.width+2, h: -1.0]]; Imager.MaskRectangle[context, [x: 0, y: -1, w: -1.0, h: height+1]]; Imager.MaskRectangle[context, [x: data.width, y: -1, w: 1.0, h: height+1]]; FOR i: INT _ 0, i + 60 UNTIL i > data.width DO Imager.MaskRectangle[context, [x: i, y: -1, w: -1.0, h: -5.0]]; Imager.SetXY[context, [i+leading, 0+leading-headerHeight]]; ENDLOOP; }; Frame: PROC [context: Context, data: Data, max: INT] ~ { BasicFrame[context, data, data.height]; FOR i: INT _ 0, i + 30 UNTIL i > data.height DO value: INT = max*i/data.height; tag: Rope.ROPE = AbbRopeFromInt[value]; ropeWidth: VEC = ImagerFont.RopeEscapement[data.helvetica8, tag]; Imager.SetXY[context, [0-ropeWidth.x-5-5, i]]; IF i # 0 THEN Imager.ShowRope[context, tag]; Imager.MaskRectangle[context, [x: -1, y: i, w: -5.0, h: -1.0]]; Imager.MaskRectangle[context, [x: data.width+1, y: i, w: +5.0, h: -1.0]]; Imager.SetXY[context, [0+data.width+5+5, i]]; Imager.ShowRope[context, tag]; ENDLOOP; }; AbbRopeFromInt: PROC [i: INT] RETURNS [rope: Rope.ROPE] = { IF i = 0 THEN RETURN["0"]; SELECT TRUE FROM (i MOD 1000) = 0 => rope _ Rope.Cat[Convert.RopeFromInt[i/1000], "K"]; (i MOD 1000000) = 0 => rope _ Rope.Cat[Convert.RopeFromInt[i/1000000], "M"]; ENDCASE => rope _ Convert.RopeFromInt[i]; }; PlotData: PROC [context: Context, data: Data, what: REF ArrayOfPoints, max: INT, t1, t2, t3: Rope.ROPE] = { scale: REAL = REAL[max]/REAL[data.height]; Imager.TranslateT[context, [0, -(data.height + top)]]; Frame[context, data, max]; FOR i: INT IN [0..data.width) DO sample: REAL _ REAL[what[i]]/scale ; IF sample > 0 THEN Imager.SetColor[context, Imager.black] ELSE {Imager.SetColor[context, gray]; sample _ -sample}; sample _ MIN[sample, REAL[data.height]]; Imager.MaskRectangle[context, [x: i, y: 0, w: 1.0, h: sample]]; ENDLOOP; Imager.SetColor[context, Imager.black]; Title[context, data, t1, t2, t3]; Imager.TranslateT[context, [0, -bottom]]; }; RepaintViewer: ViewerClasses.PaintProc = { data: Data = NARROW[self.data]; Imager.TranslateT[context, [left, data.totalHeight]]; FOR i: INT IN [1..data.rows] DO PlotData[context, data, data.points[i-1], data.yMax, data.jukeboxName, data.tuneString, IF data.commandName = plotDistribution THEN ( IF i = 1 THEN "whole: coarse" ELSE "peak: fine" ) ELSE IO.PutFR["Line %d of %d", IO.int[i], IO.int[data.rows]]]; Imager.MaskRectangle[context, [x: -left, y: 0, w: left+data.width+left, h: 1]] ENDLOOP }; global: Data _ NIL; Poof: ViewerEvents.EventProc = { data: Data = NARROW[viewer.data]; IF global = data THEN global _ NIL; }; AddPoint: PROC [currPlotPoint: INT, data: Data, value: INT] RETURNS [nextPlotPoint: INT] = { IF currPlotPoint<0 THEN currPlotPoint_0; IF currPlotPoint>=data.rows*data.width THEN currPlotPoint_data.rows*data.width-1; data.points[currPlotPoint/data.width][currPlotPoint MOD data.width] _ value; currPlotPoint_currPlotPoint+1; RETURN [IF currPlotPoint>=data.rows*data.width THEN data.rows*data.width-1 ELSE currPlotPoint] }; LastComponentWithoutExtension: PROC [fileName: Rope.ROPE] RETURNS [simpleName: Rope.ROPE] = { start, end: INT _ fileName.Length[] - 1; curr: INT; IF fileName.Fetch[start] = '> OR fileName.Fetch[start] = '/ THEN ERROR; WHILE fileName.Fetch[start-1] # '> AND fileName.Fetch[start-1] # '/ AND start > 0 DO start _ start - 1 ENDLOOP; curr _ start; IF fileName.Fetch[start] = '. OR fileName.Fetch[start] = '! THEN ERROR; WHILE curr < end DO IF fileName.Fetch[curr+1] = '. OR fileName.Fetch[curr+1] = '! THEN end _ curr ELSE curr _ curr + 1 ENDLOOP; RETURN [fileName.Substr[start, end-start+1]] }; ParseTuneProfile: PROC [data: Data, cmd: Commander.Handle] RETURNS [succeeded: BOOLEAN _ FALSE, error: Rope.ROPE _ NIL] = TRUSTED { weOpened: BOOL _ FALSE; jukebox: Jukebox.Handle _ NIL; tune: Jukebox.Tune _ NIL; {{ ENABLE Jukebox.Error => { error _ rope; GOTO Quit }; argv: CommandTool.ArgumentVector; tuneSize: INT; tuneID: INT; sampleIncrement: INT; pointsPerChirp: INT; currPlotPoint: INT _ 0; peakEnergy: Jukebox.EnergyRange _ 0; argv _ CommandTool.Parse[cmd ! CommandTool.Failed => {error _ errorMsg; GOTO Quit}]; IF argv.argc < 3 OR argv.argc > 4 THEN { error _ "Usage: NewPlotTune jukeboxName|# tuneNumber [displayRows _ 8secondsPerRow]"; GOTO Quit; }; IF argv[1].Equal["#"] THEN { IF Jukebox.instances=NIL THEN { error _ "No jukebox open"; GOTO Quit}; jukebox _ Jukebox.instances.first; } ELSE jukebox _ Jukebox.FindJukebox[argv[1]]; IF jukebox = NIL THEN { jukebox _ Jukebox.OpenJukebox[argv[1]]; weOpened _ TRUE }; tuneID _ Convert.IntFromRope[argv[2]]; tune _ Jukebox.OpenTune[self: jukebox, tuneId: tuneID, write: FALSE]; tuneSize _ Jukebox.TuneSize[tune]; data.jukeboxName _ LastComponentWithoutExtension[jukebox.jukeboxName]; IF tuneSize = 0 THEN {error _ "Tune contains no chirps, so I won't plot them"; GOTO Quit}; data.rows _ IF argv.argc = 3 THEN MAX[(tuneSize+4)/8, 1] ELSE Convert.IntFromRope[argv[3]]; IF data.rows > maxRows THEN data.rows _ maxRows; IF data.rows = 0 THEN {error _ "It didn't take me long to plot no rows!"; GOTO Quit}; sampleIncrement _ tuneSize*Jukebox.bytesPerChirp/(data.rows*maxWidth); IF sampleIncrement > Jukebox.bytesPerChirp -- some tune: will have to add more rows !! THEN { data.rows _ (tuneSize + (maxWidth -1))/maxWidth; sampleIncrement _ Jukebox.bytesPerChirp; IF data.rows > maxRows THEN ERROR; data.width _ (tuneSize+(data.rows-1))/data.rows } ELSE { WHILE (Jukebox.bytesPerChirp MOD sampleIncrement) # 0 DO sampleIncrement _ sampleIncrement + 1 ENDLOOP; data.width _ ((Jukebox.bytesPerChirp/sampleIncrement)*tuneSize+(data.rows-1))/data.rows }; pointsPerChirp _ Jukebox.bytesPerChirp/sampleIncrement; data.totalHeight _ data.rows*(top+data.height+bottom); FOR i: INT IN [0..data.rows) DO data.points[i] _ NEW [ArrayOfPoints[data.width]] ENDLOOP; FOR chirp: INT IN [0..tuneSize) DO energyBlock: TuneAccess.EnergyBlock _ TuneAccess.GetEnergyProfile[jukebox, tune, chirp, , pointsPerChirp]; FOR element: INT IN [0..pointsPerChirp) DO { peakEnergy _ MAX [peakEnergy, energyBlock[element]]; currPlotPoint _ AddPoint[currPlotPoint: currPlotPoint, data: data, value: energyBlock[element]] } ENDLOOP ENDLOOP; IF peakEnergy = 0 THEN {error _ "No energy in this file at all!"; GOTO Quit}; { positionInChirp: INT _ 0; -- used to tell when to get a new ambient level hangoverInPoints: INT _ Jukebox.hangoverPackets*pointsPerChirp/ INT[(Jukebox.bytesPerChirp/Jukebox.singlePktLength)]; pointsSinceLastNonSilence: INT _ hangoverInPoints; ambientLevel: Jukebox.EnergyRange; currChirp: INT _ 0; FOR i: INT IN [0..data.rows) DO FOR j: INT IN [0..data.width) DO IF positionInChirp = 0 THEN { ambientLevel _ TuneAccess.ReadAmbientLevel[jukebox, tune, currChirp ! Jukebox.MissingChirp => {ambientLevel _ LAST[Jukebox.EnergyRange]; CONTINUE}; Jukebox.EOF => CONTINUE]; -- latter may occur if there are more points in the graph than pointsPerChirp*lengthInChirps due to integer division etc. currChirp _ currChirp + 1 }; pointsSinceLastNonSilence _ IF data.points[i][j] <= ambientLevel THEN pointsSinceLastNonSilence + 1 ELSE 0; IF pointsSinceLastNonSilence > hangoverInPoints THEN data.points[i][j] _ -data.points[i][j]; positionInChirp _ (positionInChirp+1) MOD pointsPerChirp ENDLOOP ENDLOOP }; Jukebox.ArchiveCloseTune[jukebox, tune]; IF weOpened THEN jukebox _ Jukebox.CloseJukebox[jukebox]; cmd.out.PutF["Energy peak in file is %d - graph is scaled accordingly\n", IO.int[peakEnergy]]; data.yMax _ peakEnergy; data.tuneString _ Rope.Cat[IF tuneID < 10 THEN "Tune " ELSE "Tune", Convert.RopeFromInt[tuneID]]; data.tuneNumberFont _ IF tuneID < 100 THEN data.helvetica10 ELSE data.helvetica8; RETURN[TRUE] }; EXITS Quit => { IF tune # NIL THEN Jukebox.ArchiveCloseTune[jukebox, tune]; IF weOpened AND jukebox # NIL THEN jukebox _ Jukebox.CloseJukebox[jukebox]; RETURN [FALSE, error] }; }}; PlotTune: Commander.CommandProc = { data: Data _ NEW [DataRecord]; parsedOkay: BOOLEAN; data.commandName _ plotTune; data.height _ 60; [parsedOkay, msg] _ ParseTuneProfile[data, cmd]; IF ~parsedOkay THEN RETURN [$Failure, msg]; data.container _ Containers.Create[[ name: "Tune Energy Time Profile", iconic: FALSE, icon: Icons.NewIconFromFile["JukeboxIcons.icon", 10], -- pro tem., until Polle produces a graph column: left, menu: NIL, scrollable: FALSE, data: data ]]; ViewerOps.SetOpenHeight[data.container, data.totalHeight]; data.graphs _ ViewerOps.CreateViewer[ flavor: $TunePlot, info: [ parent: data.container, wx: 0, wy: 0, ww: left+maxWidth+left, -- this means the window is still standard left-column width, even though the graph may be narrower: this is judged more visually pleasing wh: data.totalHeight, scrollable: FALSE, data: data] ]; [] _ ViewerEvents.RegisterEventProc[Poof, destroy, data.graphs, TRUE]; }; EnergyArray: TYPE = REF EnergyValueArray; EnergyValueArray: TYPE = ARRAY Jukebox.EnergyRange OF INT; ParseTuneEnergies: PROC [data: Data, cmd: Commander.Handle] RETURNS [succeeded: BOOLEAN _ FALSE, error: Rope.ROPE _ NIL] = TRUSTED { weOpened: BOOL _ FALSE; jukebox: Jukebox.Handle _ NIL; tune: Jukebox.Tune _ NIL; {{ ENABLE Jukebox.Error => { error _ rope; GOTO Quit }; argv: CommandTool.ArgumentVector; tuneSize: INT; tuneID: INT; currPlotPoint: INT _ 0; energyMax: Jukebox.EnergyRange _ 0; energyPeak: Jukebox.EnergyRange _ 0; levelsPerCoarsePixel: INT; energyProfile: EnergyArray _ NEW [EnergyValueArray _ ALL[0]]; fineLower, fineUpper: Jukebox.EnergyRange; argv _ CommandTool.Parse[cmd ! CommandTool.Failed => {error _ errorMsg; GOTO Quit}]; IF argv.argc # 3 THEN { error _ "Usage: NewPlotTune jukeboxName|# tuneNumber"; GOTO Quit }; IF argv[1].Equal["#"] THEN { IF Jukebox.instances=NIL THEN { error _ "No jukebox open"; GOTO Quit}; jukebox _ Jukebox.instances.first; } ELSE jukebox _ Jukebox.FindJukebox[argv[1]]; IF jukebox = NIL THEN { jukebox _ Jukebox.OpenJukebox[argv[1]]; weOpened _ TRUE }; tuneID _ Convert.IntFromRope[argv[2]]; tune _ Jukebox.OpenTune[self: jukebox, tuneId: tuneID, write: FALSE]; tuneSize _ Jukebox.TuneSize[tune]; data.jukeboxName _ LastComponentWithoutExtension[jukebox.jukeboxName]; IF tuneSize = 0 THEN {error _ "Tune contains no chirps, so I won't plot them"; GOTO Quit}; FOR chirp: INT IN [0..tuneSize) DO energyBlock: TuneAccess.EnergyBlock _ TuneAccess.GetEnergyProfile[jukebox, tune, chirp, , Jukebox.bytesPerChirp/Jukebox.singlePktLength]; FOR element: INT IN [0..Jukebox.bytesPerChirp/Jukebox.singlePktLength) DO energyProfile[energyBlock[element]] _ energyProfile[energyBlock[element]] + 1 ENDLOOP ENDLOOP; energyProfile[0] _ 0; FOR i: Jukebox.EnergyRange IN Jukebox.EnergyRange DO IF energyProfile[i] > energyProfile[energyPeak] THEN energyPeak _ i; IF energyProfile[i] > 0 THEN energyMax _ i ENDLOOP; levelsPerCoarsePixel _ ((energyMax+1) + (maxWidth-1))/maxWidth; data.width _ (energyMax+levelsPerCoarsePixel)/levelsPerCoarsePixel; data.rows _ 2; data.totalHeight _ data.rows*(top+data.height+bottom); FOR i: INT IN [0..data.rows) DO data.points[i] _ NEW [ArrayOfPoints[data.width]] ENDLOOP; { currEnergy: Jukebox.EnergyRange _ 0; FOR i: INT IN [0..data.width-2] DO accumulator: INT _ 0; FOR j: INT IN [0..levelsPerCoarsePixel) DO accumulator _ accumulator + INT[energyProfile[currEnergy]]; currEnergy _ currEnergy + 1 ENDLOOP; data.points[0][i] _ REAL[accumulator]/REAL[levelsPerCoarsePixel] ENDLOOP; { lastPoint: INT _ 0; DO lastPoint _ lastPoint + INT[energyProfile[currEnergy]]; IF currEnergy = energyMax THEN EXIT; currEnergy _ currEnergy + 1 ENDLOOP; data.points[0][data.width-1] _ REAL[lastPoint]/REAL[levelsPerCoarsePixel] }}; SELECT INT[energyPeak] FROM > LAST[Jukebox.EnergyRange] - (data.width+5)/6 => { fineUpper _ LAST[Jukebox.EnergyRange]; fineLower _ fineUpper - data.width/3 + 1 }; < FIRST[Jukebox.EnergyRange] + (data.width+5)/6 => { fineLower _ FIRST[Jukebox.EnergyRange]; fineUpper _ fineLower + data.width/3 - 1 }; ENDCASE => { fineLower _ energyPeak - data.width/6; fineUpper _ fineLower + data.width/3 - 1 }; FOR i: INT IN [0..data.width) DO data.points[1][i] _ energyProfile[fineLower+i/3] ENDLOOP; FOR i: INT IN [fineLower/levelsPerCoarsePixel..fineUpper/levelsPerCoarsePixel] DO IF i <= LAST[Jukebox.EnergyRange] THEN data.points[0][i] _ -data.points[0][i] ENDLOOP; Jukebox.ArchiveCloseTune[jukebox, tune]; IF weOpened THEN jukebox _ Jukebox.CloseJukebox[jukebox]; cmd.out.PutF["Highest energy value in file is %d: this is length of coarse plot x axis\nMost frequent energy level is %d occurences of %d:\n fine plot x axis runs from %d to %d (range shown gray on coarse plot)\n", IO.int[energyMax], IO.int[energyProfile[energyPeak]], IO.int[energyPeak], IO.int[fineLower], IO.int[fineUpper]]; data.yMax _ energyProfile[energyPeak]; data.tuneString _ Rope.Cat[IF tuneID < 10 THEN "Tune " ELSE "Tune", Convert.RopeFromInt[tuneID]]; data.tuneNumberFont _ IF tuneID < 100 THEN data.helvetica10 ELSE data.helvetica8; RETURN[TRUE] }; EXITS Quit => { IF tune # NIL THEN Jukebox.ArchiveCloseTune[jukebox, tune]; IF weOpened AND jukebox # NIL THEN jukebox _ Jukebox.CloseJukebox[jukebox]; RETURN [FALSE, error] }; }}; PlotDistribution: Commander.CommandProc = { data: Data _ NEW [DataRecord]; parsedOkay: BOOLEAN; data.commandName _ plotDistribution; data.height _ 150; [parsedOkay, msg] _ ParseTuneEnergies[data, cmd]; IF ~parsedOkay THEN RETURN [$Failure, msg]; data.container _ Containers.Create[[ name: "Tune Energy Distribution", iconic: FALSE, icon: Icons.NewIconFromFile["JukeboxIcons.icon", 15], -- pro tem., until Polle produces a graph icon column: left, menu: NIL, scrollable: FALSE, data: data ]]; ViewerOps.SetOpenHeight[data.container, data.totalHeight]; data.graphs _ ViewerOps.CreateViewer[ flavor: $TunePlot, info: [ parent: data.container, wx: 0, wy: 0, ww: left+maxWidth+left, -- this means the window is still standard left-column width, even though the graph may be narrower: this is judged more visually pleasing wh: data.totalHeight, scrollable: FALSE, data: data] ]; [] _ ViewerEvents.RegisterEventProc[Poof, destroy, data.graphs, TRUE]; }; graphClass: ViewerClasses.ViewerClass _ NEW[ViewerClasses.ViewerClassRec _ [paint: RepaintViewer]]; ViewerOps.RegisterViewerClass[$TunePlot, graphClass]; Commander.Register["PlotTune", PlotTune, "PlotTune jukeboxName|# tuneNumber [displayRows _ 8secondsPerRow] - plot the content of a tune in graphical form: # represents the first currently open jukebox"]; Commander.Register["PlotDistribution", PlotDistribution, "PlotDistribution jukeboxName|# tuneNumber - plot the distribution of energies in a jukebox tune: # represents the first currently open jukebox"]; }. TuneGraphsImpl.mesa programs to plot the energy profile and distribution of a tune Ades, March 6, 1986 4:34:12 pm PST Swinehart, April 7, 1987 5:41:05 pm PDT variables and routines used by both commands [self: ViewerClasses.Viewer, context: Imager.Context, whatChanged: REF ANY, clear: BOOL] RETURNS [quit: BOOL _ FALSE] routines used to implement PlotTune only because of the way that Tuneaccess.GetEnergyProfile works, we want to sample an integral number of times per chirp. The number of samples may not add up to a multiple of maxWidth, in which case set data.width to a lower value the above paragraph built up the points on the graph. We want now to shade gray the points which are silent according to the ambient level values. The Voicestreams software samples every 20ms to do this rather than at the intervals we did above, but we'll use the sampling rate above because otherwise we'll get some odd edging effects. We'll multiply all the data points by -1 if they are to be made gray, a convention that the PlotData routine understands we use ArchiveCloseTune so that the read/write dates will not be altered by this command routines used to implement PlotDistribution only the first task is to build up an array depicting the occurences of each energy level we are not interested in the (potentially large) number of zero energy occurences program plots out two lines of information: on the first is a histogram 0..energyMax of the energy occurences [averaged over the several levels represented by a single point on this coarse plot] and on the second a plot one level per pixel plot centred on the energyPeak, i.e. frequency maximum: the area of the second is shown gray on the first. We'll set the same y-height for both axes: on the gray region we may get against-the-rails behaviour but this doen't matter handle data.width-1 specially in case of bounds problems now the fine plot and shade the fine plot regions gray on the coarse plot we use ArchiveCloseTune so that the read/write dates will not be altered by this command Κ˜šœS™SIcode™"K™'J˜—šΟk ˜ Jšœ œ!˜0Jšœ œ!˜2Jšœ œ˜%Jšœœ˜)Jšœœ˜Jšœœœ˜$Jšœœ[˜gJšœ œ˜"Jšœ œ˜.JšœœΑ˜ΞJšœœœ ˜5Jšœœœ˜Jšœ œ ˜2Jšœœ2˜EJšœ œ4˜CJšœ œ3˜CJ˜—šœœ˜š˜Jšœ4œM˜ƒJ˜—Jšœ œ˜Jšœœ œ˜Jšœœ˜Jšœ œ ˜1J˜J™,J˜Jšœœœ ˜šœ œœ˜Jšœ"œ˜&Jšœœ˜#J˜J˜Jšœœœ˜Jšœœœ˜Jšœœ˜ Jšœœ˜Jšœœ˜ Jšœœ˜ Jšœ œ˜Jš œœœœœ˜CJ˜JšœB˜BJšœB˜BJšœD˜DJ˜J˜—Jšœ œ˜Jšœ œ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœœ ˜+Jšœœ˜Jšœ œ˜Jšœ8˜8Jšœ œΟcL˜^J˜Jš œœœœœ˜BJšœœœ˜'Jš œœœœœœ˜NJ˜šΟnœœ1œ˜EJšœ)˜)JšœE˜EJšœ˜Jšœ-˜-Jšœ8˜8Jšœ˜Jšœ)˜)JšœG˜GJšœ˜Jšœ˜J˜—šŸ œœ(œ˜@Jšœ)˜)JšœG˜GJšœC˜CJšœK˜Kšœœ œ˜/Jšœ?˜?Jšœ;˜;Jšœ˜—Jšœ˜J˜—šŸœœ%œ˜8Jšœ'˜'šœœ œ˜/Jšœœ˜Jšœ œ˜'Jšœ œ3˜AJšœ.˜.Jšœœ˜,Jšœ?˜?JšœI˜IJšœ-˜-Jšœ˜Jšœ˜—Jšœ˜J˜—š Ÿœœœœ œ˜;Jšœœœ˜šœœ˜Jšœœ@˜FJšœœF˜LJšœ"˜)—Jšœ˜J˜—š Ÿœœ&œœœ˜kJšœœœœ˜*Jšœ6˜6Jšœ˜šœœœ˜ Jšœœœ˜$Jšœ œ(œ4˜sJšœ œ œ˜(Jšœ?˜?—Jšœ˜Jšœ'˜'Jšœ!˜!Jšœ)˜)Jšœ˜J˜—šœ*˜*JšΠcku™uJšœ œ ˜Jšœ5˜5J˜šœœœ˜JšœXœ%œœœœœœœ œ˜χJšœN˜N—Jš˜J˜—Jšœœ˜šΟbœ˜ Jšœ œ˜!Jšœœ œ˜#Jšœ˜J˜—š Ÿœœœœœœ˜\Jšœœ˜(Jšœ%œ&˜QJšœ4œ˜LJ˜Jšœœ%œœ˜^—˜J˜—š Ÿœœœœœ˜]Jšœ œ˜(Jšœœ˜ Jšœœœœ˜GJš œœœ œœ˜oJšœ ˜ Jšœœœœ˜Gšœ ˜Jšœœœ œ˜b—Jšœ˜Jšœ'˜-—Jšœ˜J˜™(J˜—šŸœœ%œ œœœœœ˜ƒJšœ œœ˜Jšœœ˜Jšœœ˜J˜šœ˜Jšœœ˜J˜—Jšœ!˜!Jšœ œ˜Jšœœ˜ Jšœœ˜Jšœœ˜Jšœœ˜J˜$J˜JšœHœ˜Tšœœœ˜)JšœU˜UJšœ˜ J˜—šœœ˜Jšœœœœ˜FJšœ"˜"J˜—Jšœ(˜,šœ œœ˜J˜'Jšœ ˜J˜—Jšœ&˜&Jšœ>œ˜EJšœ"˜"J˜JšœF˜FJ˜Jšœœ;œ˜ZJš œ œœœœ˜[Jšœœ˜0Jšœœ5œ˜UJ˜Jšœα™αJšœF˜FJšœ)ž+˜VJšœ˜šœ3˜3Jšœ(˜(Jšœœœ˜"Jšœ/˜/—J˜Jšœ˜šœ˜Jšœœœ'œ˜gJšœW˜W—J˜J˜Jšœ7˜7J˜Jšœ6˜6šœœœ˜Jšœœ˜0—Jšœ˜J˜šœœœ˜"Jšœj˜jJšœ œœ˜*šœœ$˜7Jšœ_˜_—J˜Jš˜—Jšœ˜J˜Jšœœ,œ˜MJ˜J™Ιšœœž/˜LJšœœ+œ2˜uJšœœ˜2Jšœ"˜"Jšœ œ˜J˜šœœœ˜šœœœ˜ Jšœœ˜šœ˜Jš œnœœ œœžy˜§J˜—J˜Jšœœ#œ œ˜lJšœ.œ(˜\Jšœ&œ˜8—Jš˜—Jš˜—˜J˜—J˜(JšœX™XJšœ œ)˜9JšœJœ˜^J˜Jšœ˜Jšœa˜aJšœR˜RJšœœ˜ J˜Jš˜šœ ˜ Jšœœœ)˜;Jšœ œ œœ)˜KJšœœ˜Jšœ˜—J˜—J˜š‘œ˜#Jšœ œ˜Jšœ œ˜J˜J˜J˜Jšœ0˜0Jšœ œœ˜+J˜šœ$˜$Jšœ!˜!Jšœœ˜Jšœ6ž)˜_Jšœ ˜ Jšœœ˜ Jšœ œ˜Jšœ˜—Jšœ:˜:šœ%˜%Jšœ˜šœ˜Jšœ˜Jšœ˜Jšœ˜JšœžŠ˜’Jšœ˜Jšœ œ˜Jšœ˜——Jšœ@œ˜FJšœ˜—J˜J™0J™Jšœ œœ˜)š œœœœœ˜:J˜—šŸœœ%œ œœœœœ˜„Jšœ œœ˜Jšœœ˜Jšœœ˜J˜šœ˜Jšœœ˜J˜—Jšœ!˜!Jšœ œ˜Jšœœ˜ Jšœœ˜J˜#J˜$Jšœœ˜Jšœœœ˜=J˜*J˜JšœHœ˜Tšœœ˜Jšœ6˜6Jšœ˜ J˜—šœœ˜Jšœœœœ˜FJšœ"˜"J˜—Jšœ(˜,šœ œœ˜J˜'Jšœ ˜Jšœ˜—Jšœ&˜&Jšœ>œ˜EJšœ"˜"J˜JšœF˜FJ˜Jšœœ;œ˜ZJ˜J™Tšœœœ˜"Jšœ‰˜‰šœ œœ4˜IJšœN˜N—Jš˜—Jšœ˜ J™QJšœ˜—˜šœœ˜4Jšœ.œ˜DJšœœ˜*—Jšœ˜J˜JšœΧ™ΧJ˜Jšœ?˜?JšœC˜CJšœ˜Jšœ6˜6šœœœ˜Jšœœ˜0—Jšœ˜J˜˜'Jšœœœœ˜#šœ8™8Jšœ œ˜šœœœ˜*Jšœœ˜;J˜—Jšœ˜Jšœœœ˜@—Jšœ˜—˜Jšœ œ˜Jš œœœœœœ˜…Jšœœ œ˜I—Jšœ˜J˜J™Jšœœ ˜šœœ+˜1šœœ˜*Jšœ+˜+——šœœ+˜2šœœ˜+Jšœ+˜+——šœ˜ šœ*˜*šœ+˜+J˜———šœœœ˜ Jšœ0˜0—Jšœ˜J˜J™7šœœœB˜QJšœœœ'˜N—Jšœ˜J˜J˜(JšœX™XJšœ œ)˜9Jš œΧœœ!œœœ˜ΗJ˜Jšœ&˜&Jšœa˜aJšœQ˜QJšœœ˜ J˜Jš˜šœ ˜ Jšœœœ)˜;Jšœ œ œœ)˜KJšœœ˜Jšœ˜—J˜—J˜š‘œ˜+Jšœ œ˜Jšœ œ˜J˜$J˜J˜Jšœ1˜1Jšœ œœ˜+J˜šœ$˜$Jšœ!˜!Jšœœ˜Jšœ6ž.˜dJšœ ˜ Jšœœ˜ Jšœ œ˜Jšœ˜—Jšœ:˜:šœ%˜%Jšœ˜šœ˜Jšœ˜Jšœ˜Jšœ˜JšœžŠ˜’Jšœ˜Jšœ œ˜Jšœ˜——Jšœ@œ˜FJšœ˜J˜—šœ(œ˜JJšœ˜—Jšœ5˜5JšœΛ˜ΛJšœΛ˜ΛJ˜Jšœ˜——…—DΤ^τ