DIRECTORY Atom, GraphBrowsers, Graphs, Icons, Imager, ImagerBackdoor, ImagerBox, ImagerFont, Menus, MessageWindow, MJSContainers, Process, Real, Rope, TIPUser, VFonts, ViewerClasses, ViewerOps; GraphBrowsersImpl: CEDAR MONITOR IMPORTS Atom, Graphs, Icons, Imager, ImagerBackdoor, ImagerBox, ImagerFont, MessageWindow, MJSContainers, Process, Real, Rope, TIPUser, VFonts, ViewerOps EXPORTS GraphBrowsers = BEGIN OPEN GraphBrowsers; Edge: TYPE = Graphs.Edge; Direction: TYPE = Graphs.Direction; nullVertex: Vertex = Graphs.nullVertex; Pane: TYPE = REF PaneRep; PaneRep: PUBLIC TYPE = RECORD [ asViewer: Viewer, vertex: Vertex, browser: Browser, parent: Pane, myFilter: Graphs.DirectionFilter, myIndex: CARDINAL, height: CARDINAL, columnWidth: REAL, rows, columns: CARDINAL, expanded, expandedV: CARDINAL, length: CARDINAL, sel, vSel: CardBox, maxNameWidth: REAL, entries: SEQUENCE size: CARDINAL OF PaneEntry]; CardBox: TYPE = RECORD [ startRow, startCol, finishRow, finishCol: CARDINAL]; badBox: CardBox = [badRowCol, badRowCol, badRowCol, badRowCol]; badRowCol: CARDINAL = LAST[CARDINAL] - 1; PaneEntry: TYPE = RECORD [ paneText, edgeLabel, vertexLabel: ROPE _ NIL, vertex: Vertex _ nullVertex, drawX, drawY: REAL _ 0.0]; Browser: TYPE = REF BrowserRep; BrowserRep: PUBLIC TYPE = RECORD [ container: Viewer _ NIL, width: INTEGER _ 0, panes: LIST OF Pane _ NIL, class: BrowserClass, browserData: REF ANY, serial: INTEGER _ 1, newSerial: CONDITION]; dy: INT _ VFonts.FontHeight[] + 2; hSep: REAL _ 10; font: ImagerFont.Font _ VFonts.defaultFont; paneFlavor: ATOM _ Atom.MakeAtom["Spreitzer January 5, 1984 6:40 pm"]; icon: Icons.IconFlavor _ Icons.NewIconFromFile["GraphBrowsers.icons", 0]; basicBrowserClass: PUBLIC BrowserClass _ NEW [BrowserClassRep _ [ cmds: [ ALL[[[Expand, $Outgoing], [Contract]]], ALL[[[Expand, $Both], [Contract]]], ALL[[[Expand, $Incoming], [Contract]]] ] ]]; paneClass: ViewerClasses.ViewerClass _ NEW [ViewerClasses.ViewerClassRec _ [ flavor: paneFlavor, notify: NotifyPane, paint: PaintPane, tipTable: TIPUser.InstantiateNewTIPTable[file: "GraphBrowserPane.TIP"], topDownCoordSys: FALSE ]]; Browse: PUBLIC PROC [vertex: Vertex, browserClass: BrowserClass, browserData: REF ANY _ NIL, viewerInit: ViewerClasses.ViewerRec _ [], directionFilter: Graphs.DirectionFilter _ Graphs.allDirections, paint: BOOLEAN _ TRUE] RETURNS [browser: Browser] = BEGIN browser _ NEW [BrowserRep _ [ class: browserClass, browserData: browserData]]; viewerInit.data _ browser; IF viewerInit.icon = unInit THEN viewerInit.icon _ icon; browser.container _ MJSContainers.Create[viewerFlavor: $GraphBrowser, info: viewerInit, paint: paint]; TRUSTED {Process.InitializeCondition[@browser.newSerial, Process.SecondsToTicks[3]]}; browser.panes _ LIST[MakePane[ browser: browser, vertex: vertex, parent: NIL, myIndex: 0, directionFilter: directionFilter, paint: paint ]]; END; BrowserQuaViewer: PUBLIC PROC [browser: Browser] RETURNS [Viewer] = {RETURN [browser.container]}; MakePane: PROC [browser: Browser, vertex: Vertex, parent: Pane, myIndex: CARDINAL, directionFilter: Graphs.DirectionFilter, paint: BOOLEAN] RETURNS [pane: Pane] = BEGIN SurveyNameWidths: PROC [direction: Direction, edgeLabel, vertexLabel: ROPE] = { paneText: ROPE = MakePaneText[direction, edgeLabel, vertexLabel]; xmin,ymin,xmax,ymax: REAL; [[xmin,ymin,xmax,ymax]] _ ImagerBox.BoxFromExtents[font.RopeBoundingBox[paneText]]; pane.entries[index] _ [paneText: paneText, edgeLabel: edgeLabel, vertexLabel: vertexLabel, drawX: 0, drawY: 0, vertex: nullVertex]; pane.maxNameWidth _ MAX[pane.maxNameWidth, hSep + xmax - xmin]; index _ index + 1; }; SurveyVertexWidths: PROC [edge: Edge, direction: Direction, otherSide: Vertex] = { edgeLabel: ROPE = edge.GetEdgeLabel[]; vertexLabel: ROPE = otherSide.GetVertexLabel[]; paneText: ROPE = MakePaneText[direction, edgeLabel, vertexLabel]; xmin,ymin,xmax,ymax: REAL; [[xmin,ymin,xmax,ymax]] _ ImagerBox.BoxFromExtents[font.RopeBoundingBox[paneText]]; pane.entries[index] _ [paneText: paneText, edgeLabel: edgeLabel, vertexLabel: vertexLabel, drawX: 0, drawY: 0, vertex: otherSide]; pane.maxNameWidth _ MAX[pane.maxNameWidth, hSep + xmax - xmin]; index _ index + 1; }; Elt: TYPE = RECORD [edge: Edge, direction: Direction, otherSide: Vertex]; GetList: PROC [edge: Edge, direction: Direction, otherSide: Vertex] = BEGIN rest: LIST OF Elt _ LIST[[edge, direction, otherSide]]; IF last = NIL THEN list _ rest ELSE last.rest _ rest; last _ rest; size _ size + 1; END; list, last: LIST OF Elt _ NIL; index, size: CARDINAL _ 0; IF vertex.class.GetNeighborCount = NIL THEN TRUSTED {vertex.class.Expand[vertex, GetList, directionFilter]} ELSE size _ vertex.class.GetNeighborCount[vertex, directionFilter]; pane _ NEW [PaneRep[size]]; pane.browser _ browser; pane.parent _ parent; pane.myFilter _ directionFilter; pane.myIndex _ myIndex; pane.vertex _ vertex; pane.height _ 10; pane.expanded _ pane.expandedV _ pane.size; pane.sel _ pane.vSel _ badBox; pane.columnWidth _ 0; pane.maxNameWidth _ 0; IF (size = 0) OR list # NIL THEN BEGIN FOR list _ list, list.rest WHILE list # NIL DO SurveyVertexWidths[list.first.edge, list.first.direction, list.first.otherSide]; ENDLOOP; END ELSE vertex.EnumerateForLabels[SurveyNameWidths, directionFilter]; pane.length _ index; pane.asViewer _ ViewerOps.CreateViewer[ flavor: paneFlavor, info: [ parent: browser.container, data: pane, ww: browser.container.cw, scrollable: FALSE], paint: FALSE]; LayoutPane[pane, paint]; END; MakePaneText: PROC [direction: Direction, edgeLabel, vertexLabel: ROPE] RETURNS [en: ROPE] = { en _ SELECT direction FROM Incoming => "_", Outgoing => "->", Undirected => "-", ENDCASE => ERROR; IF vertexLabel.Length[] # 0 THEN en _ en.Cat[" ", vertexLabel]; IF edgeLabel.Length[] # 0 THEN en _ edgeLabel.Cat[" ", en]; }; LayoutPane: PROC [pane: Pane, paint: BOOLEAN] = BEGIN index: CARDINAL _ 0; paneWidth: REAL _ 0; pane.columns _ MAX[1, Real.FixC[(paneWidth _ MAX[pane.asViewer.cw, 100]) / MAX[pane.maxNameWidth, 1]]]; pane.columnWidth _ paneWidth / pane.columns; pane.rows _ ((pane.length + pane.columns - 1) / pane.columns); pane.height _ pane.rows * dy; FOR index IN [0 .. pane.length) DO leftX, bottomY: REAL; xmin,ymin,xmax,ymax: REAL; leftX _ pane.columnWidth * (index MOD pane.columns); bottomY _ pane.height + 1 - (1 + index / pane.columns) * dy; [[xmin,ymin,xmax,ymax]] _ ImagerBox.BoxFromExtents[font.RopeBoundingBox[pane.entries[index].paneText]]; pane.entries[index].drawX _ leftX - xmin; pane.entries[index].drawY _ bottomY - ymin; ENDLOOP; ViewerOps.MoveViewer[viewer: pane.asViewer, paint: paint, x: pane.asViewer.wx, y: IF pane.parent # NIL THEN pane.parent.asViewer.wy + pane.parent.asViewer.wh ELSE 0, w: pane.asViewer.ww, h: pane.height + pane.asViewer.wh - pane.asViewer.ch]; END; LayoutBrowser: PROC [browser: Browser] = BEGIN topDown: LIST OF Pane _ NIL; FOR rest: LIST OF Pane _ browser.panes, rest.rest WHILE rest # NIL DO topDown _ CONS[rest.first, topDown]; ENDLOOP; FOR topDown _ topDown, topDown.rest WHILE topDown # NIL DO ViewerOps.MoveViewer[viewer: topDown.first.asViewer, paint: FALSE, x: topDown.first.asViewer.wx, y: topDown.first.asViewer.wy, w: browser.container.cw, h: topDown.first.asViewer.wh]; LayoutPane[topDown.first, FALSE]; ENDLOOP; END; SizeChange: PROC [self: Viewer] RETURNS [adjusted: BOOL _ FALSE] --ViewerClasses.AdjustProc-- = BEGIN browser: Browser _ NARROW[MJSContainers.GetClientData[self]]; IF browser.container = NIL THEN --we hope it's only because we're in initialization code-- RETURN; IF (browser.width # browser.container.cw) THEN BEGIN browser.width _ browser.container.cw; LayoutBrowser[browser]; END; END; SortBox: PROC [cb: CardBox] RETURNS [sorted: CardBox] = BEGIN [sorted.startCol, sorted.finishCol] _ MinMax[cb.startCol, cb.finishCol]; [sorted.startRow, sorted.finishRow] _ MinMax[cb.startRow, cb.finishRow]; END; MinMax: PROC [a, b: CARDINAL] RETURNS [min, max: CARDINAL] = {IF a <= b THEN RETURN [a, b] ELSE RETURN [b, a]}; PaintPane: PROC [self: Viewer, context: Imager.Context, whatChanged: REF, clear: BOOL] RETURNS [quit: BOOL _ FALSE] --ViewerClasses.PaintProc-- = BEGIN InvertEntry: PROC [index: CARDINAL] = BEGIN IF index IN [firstIndex .. afterLastIndex) THEN BEGIN xmin,ymin,xmax,ymax: REAL; expanded: PaneEntry _ pane.entries[index]; [[xmin,ymin,xmax,ymax]] _ ImagerBox.BoxFromExtents[font.RopeBoundingBox[expanded.paneText]]; context.MaskRectangle[[ x: xmin + expanded.drawX, w: xmax - xmin, y: ymin + expanded.drawY, h: ymax - ymin]]; END; END; InvertBox: PROC [cb: CardBox] = BEGIN context.MaskRectangle[[ x: cb.startCol * pane.columnWidth, w: (cb.finishCol+1 - cb.startCol) * pane.columnWidth, y: pane.height - (cb.finishRow+1) * dy, h: (cb.finishRow+1 - cb.startRow) * dy]]; END; pane: Pane _ NARROW[self.data]; box: Box _ [xmin: 0, ymin: 0, xmax: self.cw, ymax: self.ch]; firstRow: CARDINAL _ Real.FixC[MAX[pane.height - box.ymax, 0] / dy]; lastRow: CARDINAL _ Real.FixC[MAX[pane.height - box.ymin, 0] / dy]; firstIndex: CARDINAL _ firstRow * pane.columns; afterLastIndex: CARDINAL _ MIN[pane.length, (lastRow+1) * pane.columns]; IF clear THEN BEGIN context.SetFont[font]; FOR index: CARDINAL IN [firstIndex .. afterLastIndex) DO entry: PaneEntry _ pane.entries[index]; context.SetXY[[x: entry.drawX, y: entry.drawY]]; context.ShowRope[entry.paneText]; ENDLOOP; context.SetColor[ImagerBackdoor.invert]; InvertEntry[pane.expandedV _ pane.expanded]; InvertBox[SortBox[pane.vSel _ pane.sel]]; END ELSE BEGIN context.SetColor[ImagerBackdoor.invert]; IF pane.expandedV # pane.expanded THEN {InvertEntry[pane.expandedV]; InvertEntry[pane.expandedV _ pane.expanded]}; IF pane.vSel # pane.sel THEN {InvertBox[SortBox[pane.vSel]]; InvertBox[SortBox[pane.vSel _ pane.sel]]}; END; END; Box: TYPE = RECORD [xmin, ymin, xmax, ymax: REAL]; NotifyPane: PROC [self: Viewer, input: LIST OF REF ANY] --ViewerClasses.NotifyProc-- = BEGIN op: ATOM _ NARROW[input.first]; coords: TIPUser.TIPScreenCoords; ctled, shifted, paint: BOOLEAN _ FALSE; button: Menus.MouseButton; pane: Pane _ NARROW[self.data]; row, col: CARDINAL; sel: CardBox; IF op = $Abort THEN BEGIN pane.sel _ badBox; ViewerOps.PaintViewer[viewer: pane.asViewer, hint: client, clearClient: FALSE]; RETURN; END; coords _ NARROW[input.rest.first]; row _ Real.FixC[(pane.height - coords.mouseY) / dy]; row _ MIN[row, MAX[1, pane.rows]-1]; col _ Real.FixC[coords.mouseX / pane.columnWidth]; col _ MIN[col, MAX[1, pane.columns]-1]; IF op = $Start THEN {pane.sel.startRow _ row; pane.sel.startCol _ col; paint _ TRUE}; IF pane.sel.finishRow # row OR pane.sel.finishCol # col THEN {pane.sel.finishRow _ row; pane.sel.finishCol _ col; paint _ TRUE}; IF paint THEN ViewerOps.PaintViewer[viewer: pane.asViewer, hint: client, clearClient: FALSE]; IF op # $Finish THEN RETURN; sel _ SortBox[pane.sel]; pane.sel _ badBox; ctled _ SELECT input.rest.rest.first FROM $Ctl => TRUE, $NotCtl => FALSE, ENDCASE => ERROR; shifted _ SELECT input.rest.rest.rest.first FROM $Shift => TRUE, $NotShift => FALSE, ENDCASE => ERROR; button _ SELECT input.rest.rest.rest.rest.first FROM $Red => red, $Yellow => yellow, $Blue => blue, ENDCASE => ERROR; IF sel.startRow = badRowCol OR sel.startCol = badRowCol OR sel.finishRow = badRowCol OR sel.finishCol = badRowCol THEN MessageWindow.Append[message: "Bad selection", clearFirst: TRUE] ELSE IF pane.browser.class.cmds[button][ctled][shifted].proc # NIL THEN BEGIN MessageWindow.Append[message: NIL, clearFirst: TRUE]; IF pane.browser.class.fork THEN TRUSTED {Process.Detach[FORK Invoke[pane, sel, button, ctled, shifted]]} ELSE Invoke[pane, sel, button, ctled, shifted]; END ELSE MessageWindow.Append[message: "Undefined click", clearFirst: TRUE]; END; Invoke: PROC [pane: Pane, sel: CardBox, button: Menus.MouseButton, ctled, shifted: BOOLEAN] = BEGIN browser: Browser _ pane.browser; bc: BrowserCmd _ pane.browser.class.cmds[button][ctled][shifted]; single: BOOLEAN _ sel.startRow = sel.finishRow AND sel.startCol = sel.finishCol; IF sel = badBox THEN RETURN; IF browser.class.serialize THEN GetSerial[browser]; BEGIN ENABLE UNWIND => IF browser.class.serialize THEN ReleaseSerial[browser]; FOR col: CARDINAL IN [sel.startCol .. sel.finishCol] DO IF col >= pane.columns THEN EXIT; FOR row: CARDINAL IN [sel.startRow .. sel.finishRow] DO index: CARDINAL; IF row >= pane.rows THEN EXIT; index _ row * pane.columns + col; IF NOT index IN [0 .. pane.length) THEN {IF single THEN MessageWindow.Append[message: "Missed", clearFirst: FALSE]} ELSE BEGIN bc.proc[pane: pane, index: index, cmdData: bc.cmdData, browserData: browser.browserData, button: button, ctl: ctled, shift: shifted, paint: TRUE]; END ENDLOOP; ENDLOOP; END; IF browser.class.serialize THEN ReleaseSerial[browser]; END; GetSerial: ENTRY PROC [browser: Browser] = BEGIN WHILE browser.serial < 1 DO WAIT browser.newSerial; ENDLOOP; browser.serial _ browser.serial - 1; END; ReleaseSerial: ENTRY PROC [browser: Browser] = BEGIN browser.serial _ browser.serial + 1; BROADCAST browser.newSerial; END; GetPaneVertex: PUBLIC PROC [pane: Pane] RETURNS [vertex: Vertex] = {RETURN [pane.vertex]}; GetChildVertex: PUBLIC PROC [pane: Pane, index: CARDINAL--origin 0--] RETURNS [vertex: Vertex] = { IF pane.entries[index].vertex = nullVertex THEN { pane.entries[index].vertex _ GN[pane.vertex, pane.myFilter, pane.entries[index].edgeLabel, pane.entries[index].vertexLabel, index+1]; }; RETURN [pane.entries[index].vertex]; }; GN: PROC [v: Vertex, df: Graphs.DirectionFilter, en, vn: ROPE, index: INT] RETURNS [n: Vertex] = { e: Edge; [e, n] _ Graphs.GetNeighbor[v, df, en, vn, index]}; GetPaneUpEdge: PUBLIC PROC [pane: Pane] RETURNS [parent: Pane, index: CARDINAL--origin 0--] = {RETURN [pane.parent, pane.myIndex]}; Expand: PUBLIC PROC [pane: Pane, index: CARDINAL--origin 0--, cmdData, browserData: REF ANY, button: Menus.MouseButton, ctl, shift, paint: BOOLEAN] = BEGIN directionFilter: Graphs.DirectionFilter = SELECT cmdData FROM $Outgoing => [TRUE, FALSE, TRUE], $Both => Graphs.allDirections, $Incoming => [TRUE, TRUE, FALSE], ENDCASE => ERROR; childPane: Pane; childVertex: Vertex; IF pane.expanded = index THEN RETURN; IF pane.expanded IN [0 .. pane.length) THEN Contract[pane, pane.expanded, NIL, NIL, button, ctl, shift, FALSE]; pane.expanded _ index; childVertex _ GetChildVertex[pane, index]; IF (childVertex.class.GetNeighborCount = NIL) OR (childVertex.class.GetNeighborCount[childVertex, directionFilter] > 0) THEN BEGIN childPane _ MakePane[browser: pane.browser, vertex: childVertex, parent: pane, myIndex: index, directionFilter: directionFilter, paint: paint]; pane.browser.panes _ CONS[childPane, pane.browser.panes]; END ELSE IF paint THEN ViewerOps.PaintViewer[viewer: pane.browser.container, hint: client]; END; Contract: PUBLIC PROC [pane: Pane, index: CARDINAL--origin 0--, cmdData, browserData: REF ANY, button: Menus.MouseButton, ctl, shift, paint: BOOLEAN] = BEGIN panes: LIST OF Pane; IF index # pane.expanded THEN RETURN; FOR panes _ pane.browser.panes, panes.rest WHILE panes.first # pane DO ViewerOps.DestroyViewer[viewer: panes.first.asViewer, paint: FALSE]; ENDLOOP; pane.browser.panes _ panes; pane.expanded _ pane.length; IF paint THEN ViewerOps.PaintViewer[viewer: pane.browser.container, hint: client]; END; Setup: PROC = BEGIN ViewerOps.RegisterViewerClass[flavor: paneFlavor, class: paneClass]; MJSContainers.RegisterClass[viewerFlavor: $GraphBrowser, class: NEW [MJSContainers.MJSContainerClassRep _ [adjust: SizeChange]]]; END; Setup[]; END. NGraphBrowsersImpl.Mesa Mike Spreitzer September 27, 1986 3:16:53 pm PDT ΚΈ– "cedar" style˜™Icode™0—J˜KšΟk œΈ˜ΑK˜šΠbxœœ˜ Kšœ’˜™Kšœ˜—K˜Kšœœ˜K˜Kšœœ˜Kšœ œ˜#K˜K˜'K˜Kšœœœ ˜šœ œœœ˜K˜K˜K˜K˜ K˜!Kšœ œ˜Kšœœ˜Kšœ œ˜Kšœœ˜Kšœœ˜Kšœœ˜Kšœ˜Kšœœ˜Kšœ œœœ ˜/—K˜šœ œœ˜Kšœ*œ˜4—K˜Kšœ?˜?Kšœ œœœ˜*K˜šœ œœ˜Kšœ"œœ˜-Kšœ˜Kšœœ˜—K˜Kšœ œœ ˜šœ œœœ˜"Kšœœ˜Kšœœ˜Kšœœœœ˜Kšœ˜Kšœ œœ˜Kšœœ˜Kšœ  œ˜K˜—Kšœœ˜"Kšœœ˜K˜+Kšœ œ6˜FKšœI˜IK˜šœœœ˜Ašœ˜Kšœ$˜'Kšœ ˜#Kšœ#˜&K˜—Kšœ˜—K˜šœ'œ"˜LKšœ˜K˜K˜KšœG˜GKšœ˜Kšœ˜K˜—šΟnœœœ;œœœsœœœ˜ϊKš˜šœ œ˜Kšœ˜Kšœ˜—Kšœ˜Kšœœ˜8Kšœf˜fKšœN˜Ušœœ ˜K˜K˜Kšœœ˜ K˜ Kšœ!˜!K˜ K˜—Kšœ˜—K˜šŸœœœœ ˜CKšœœ˜K˜—š Ÿœœ;œ2œœ˜’Kš˜šŸœœ0œ˜OKšœ œ3˜AKšœœ˜KšœS˜SKšœƒ˜ƒKšœœ(˜?K˜Kšœ˜—šŸœœ:˜RKšœ œ˜&Kšœ œ˜/Kšœ œ3˜AKšœœ˜KšœS˜SKšœ‚˜‚Kšœœ(˜?K˜Kšœ˜—Kšœœœ7˜IšŸœœ8˜EKš˜Kšœœœœ˜7Kšœœœ œ˜5K˜ Kšœ˜Kšœ˜—Kšœ œœœ˜Kšœ œ˜šœ!˜&Kšœœ8˜DKšœ?˜C—Kšœœ˜K˜K˜Kšœ ˜ K˜K˜K˜Kšœ+˜+Kšœ˜K˜Kšœ˜šœ œœ˜ Kš˜šœœœ˜.KšœP˜PKšœ˜—Kš˜—Kšœ>˜BK˜šœ'˜'Kšœ˜šœ˜Kšœ˜K˜ Kšœ˜Kšœ œ˜—Kšœœ˜—K˜Kšœ˜—K˜š Ÿ œœ0œœœ˜^Kš œœ œ7œœ˜bKšœœ˜?Kšœœ˜;K˜—K˜šŸ œœœ˜/Kš˜Kšœœ˜Kšœ œ˜Kšœœœœ˜gK˜,K˜>K˜šœœ˜"Kšœœ˜Kšœœ˜Kšœ"œ˜4K˜œ˜HKšœ˜—K˜šŸœœGœ˜]Kš˜K˜ KšœA˜AKšœœ œ˜PKšœœœ˜Kšœœ˜3š˜Kšœœœœ˜Hšœœœ!˜7Kšœœœ˜!šœœœ!˜7Kšœœ˜Kšœœœ˜Kšœ!˜!šœœœ˜'Kšœœœ5œ˜K—šœ˜ KšœŒœ˜’Kš˜—Kšœ˜—Kšœ˜—Kšœ˜—Kšœœ˜7Kšœ˜—K˜šŸ œœœ˜*Kš˜šœ˜Kšœ˜Kšœ˜—K˜$Kšœ˜—K˜šŸ œœœ˜.Kš˜K˜$Kš œ˜Kšœ˜—K˜Kš Ÿ œœœœœ˜ZK˜š Ÿœœœ  œœ˜bšœ)œ˜1Kšœœf˜…Kšœ˜—Kšœ˜$Kšœ˜—K˜š œœ1œ œœ˜bK˜K˜3—K˜š Ÿ œœœœ  œ˜]Kšœœ˜%—K˜šŸœœœ  œœœ0œ˜•Kš˜šœ*œ ˜=Kšœœœœ˜!Kšœ˜Kšœœœœ˜!Kšœœ˜—K˜K˜Kšœœœ˜%Kš œœœœœœ˜oK˜Kšœ*˜*šœ'œœH˜|Kš˜Kšœ˜Kšœœ ˜9Kš˜—KšœœœE˜WKšœ˜—K˜šŸœœœ  œœœ0œ˜—Kš˜Kšœœœ˜Kšœœœ˜%šœ(œ˜FKšœ=œ˜DKšœ˜—Kšœ˜K˜KšœœE˜RKšœ˜—K˜šŸœœ˜ Kš˜KšœD˜DKšœ@œ.Οiœ˜Kšœ˜—K˜K˜K˜Kšœ˜—…—<†MŒ