Ермаков И. Е. График истории показаний. Translator: Khaliullin N.
Ermakov I. E. The chart of instrument reading history
Statement of the problem (here):
There is a little problem: let’s suppose, that the real time data readout performed on some database periodically, once per 10s for example. We need to display real-time data chart. The x-axis is vertical, the y-axis is horizontal. Chart drawing is made from the top to bottom, i.e. later data points are added to the bottom end of the curve. We need to implement the scrolling data by mouse (so, any time we could see the data, obtained X minutes ago). On the top of the chart should be the title, which doesn’t move, when we scroll data. «Come on, Oberon-guys!»
Introduction
Let’s develop a complete BlackBox view, which will support the drawing, scrolling, user input processing, saving, loading and copying. We need accurately describe required procedures and message handlers for it. All other view’s behavior will be provided by BlackBox Framework itself. It allows later to use our view inside BlackBox framework, in any master document, as you like, where you like, and with anything you like. For better understanding of further reasoning, it is recommended to read the chapters 1-5 of BlackBox documentation.
Establishing the general framework
Let's start with the overall design. As you know, one of the main principles here is to divide the problem into small, foreseeable, independent parts («divide and rule» principle, which seems, was reflected the first time in Informatics-21). This is the way, we are going to go too. Especially if we consider, that in this case it is sufficient to use the standard component programming patterns in BlackBox.
Obviously, it is necessary to separate the working with data (accumulation and storage of sensor readings) from its visual representation. This is the essence of the MVC (Model-View-Controller) pattern. However, there is no need to separately identify the third element - the controller - in this example, because we don't expect a complex interaction with the user.
So, we have to implement the model and view for our component. We’ll allocate two modules for it: MyHistoryModels and MyHistoryViews.
In addition, we’ll use a standard BlackBox method of hiding implementation. The module will export only the abstract base interface (POINTER TO ABSTRACT RECORD), and hide its implementation. The module also exports the object factory (dir: Directory), which is used to create the instance of the type. So, various factories can be installed, replacing this way the implementation, and making it transparent for all other modules. This pattern details are described in Chapter 3, «Design Techniques» of BlackBox documentation .
In our example, this approach will allow, if necessary, to develop different versions of models and visual data representations (for example, optimized for some mode of data availability). And all this models and views will be pairwise compatible with each other.
Developing the model of data storage
Let’s think over the interface, required for data accumulation and measurements reading. We continue to apply «divide and rule» principle. This suggests us the use of CRM pattern (Carrier-Rider-Mapper, Media Courier Projector) (see Chapter 3, «Design Techniques» of BlackBox documentation).
In our case, the storage allow sequentially accumulate data. Reading of data can be done by courier - Reader. There is no need to add any extra frills to the base types. For example, interpolation, if necessary, always can be realized through the wrapper-projector.
Important digression: Under any circumstances (neither in interface nor in implementation) do not try to guess all possible uses «for the future». This approach results in overcomplicated and unsustainable products (See Kalashnikov’s Principle of Informatics-21, the principles of evolution and different systematic approaches). Instead, accept the unpredictability of the future, and try to catch a small basic invariants, that may be required long enough and in many cases.
So, we get the following interface module MyHistoryModels:
DEFINITION MyHistoryModels; IMPORT Models, Stores; TYPE Model = POINTER TO ABSTRACT RECORD (Models.Model(Stores.Store)) (m: Model) AddSample (dt: INTEGER; f: REAL), NEW, ABSTRACT; (* Adding the next measured value. dt - the last time interval; f - the measured value. precondition: 20 dt > 0 *) (m: Model) Length (): INTEGER, NEW, ABSTRACT; (* The time of the last measurement (interval elapsed between the first and last measurement) *) (m: Model) NewReader (): Reader, NEW, ABSTRACT (* Create a new courier. Post-condition: reader.eod = FALSE; other fields ignored *) END; Reader = POINTER TO ABSTRACT RECORD t1, t2: INTEGER; f1, f2: REAL; (rd: Reader) Base (): Model, NEW, ABSTRACT; (rd: Reader) Read, NEW, ABSTRACT; (rd: Reader) ReadPrev, NEW, ABSTRACT; (rd: Reader) SetPos (t: INTEGER), NEW, ABSTRACT END; VAR dir-: Directory; stdDir-: Directory; PROCEDURE SetDir (d: Directory); END MyHistoryModels.
It is advisable to implement in the same module a service, that connects model to the sensor and performs periodic reading. As “sensor” we mean any procedure with signature like: SensorProc = PROCEDURE (num: INTEGER): REAL This procedure receives sensor number and returns current reading.
The connection is made not by passing a pointer to procedure (which is low-level and ugly, for the possibility to unload the module with the procedure -BlackBox won’t fall, but issues a trap), but by passing the name of procedure (it is common in Blackbox), which later can be called via meta-programming mechanisms (unit Meta) .
So, we replenish interface module with the following elements:
MODULE MyHistoryModels; ... TYPE SensorProc = PROCEDURE (num: INTEGER): REAL; ... PROCEDURE Connect (IN sensorProc: ARRAY OF CHAR; sensorNum: INTEGER; buffer: Model; interval: INTEGER; tRatio, fRatio: REAL); (* interval - reading period in milliseconds; tRatio and fRatio - time and magnitude factors, which applied to readings entered in model *) PROCEDURE Disconnect (buffer: Model);
Next, we use standard implementation of the model - the hidden in the module types StdModel, StdReader, StdDirectory, and readout services. We do the regular implementation without worrying about the speed and other performance (see degression above). If required, a module with an alternative implementation can be developed and dynamically connected, instead of the standard one (that’s why we need the factories - Directories).
MODULE MyHistoryModels; (* Case Study (С) 2008 I. Ermakov *) IMPORT Views, Models, Stores, Services, Meta, StdLog; CONST pageSize = 128*128; version = 0; TYPE Model* = POINTER TO ABSTRACT RECORD (Models.Model) sensor: Sensor END; Reader* = POINTER TO ABSTRACT RECORD t1*, t2*: INTEGER; f1*, f2*: REAL END; Directory* = POINTER TO ABSTRACT RECORD END; StdModel = POINTER TO RECORD (Model) n: INTEGER; first, last: Page END; Page = POINTER TO RECORD prev, next: Page; n: INTEGER; t: ARRAY pageSize OF INTEGER; f: ARRAY pageSize OF REAL END; StdReader = POINTER TO RECORD (Reader) base: StdModel; page: Page; n: INTEGER END; StdDirectory = POINTER TO RECORD (Directory) END; SensorProc* = PROCEDURE (num: INTEGER): REAL; Sensor = POINTER TO RECORD (Services.Action) m: Model; proc: Meta.Name; num, interval: INTEGER; lastT: LONGINT; tRatio, fRatio: REAL; it: Meta.Item; adr: SensorProc END; VAR dir-, stdDir-: Directory; (* Model *) PROCEDURE (m: Model) AddSample* (dt: INTEGER; f: REAL), NEW, ABSTRACT; (* Adding the next measured value. dt - the last time interval; f - the measured value. precondition: 20 dt > 0 *) PROCEDURE (m: Model) Length* (): INTEGER, NEW, ABSTRACT; (* Time of the last measurement (interval between the first and last measurement) *) PROCEDURE (m: Model) NewReader* (): Reader, NEW, ABSTRACT; (* Create new carrier. Post-condition: reader.eod = FALSE; Other fields ignored. *) (* Reader *) PROCEDURE (rd: Reader) Base* (): Model, NEW, ABSTRACT; (* Model, to which the carrier is linked *) PROCEDURE (rd: Reader) SetPos* (t: INTEGER), NEW, ABSTRACT; (* Read the last and previous measurements at time t. Precondition: 20 t >= 0 Post-condition: rd.t1 <= t rd.f1 - the value measured at time t1 rd.f2 - the value measured at time t2 when there are no measurements after t rd.t2 = rd.t1 when there are measurements after t (rd.t1 < rd.t2) & (rd.t <= rd.t2) *) PROCEDURE (rd: Reader) Read*, NEW, ABSTRACT; (* Read next measured value: Post-condition: rd.f1 - the value measured at time t1 rd.f2 - the value measured at time t2 if Read called for the first time after carrier creation rd.t1 = 0 if Read called not for the first time rd.t1 = rd.t2' (fn: t2' - previous t2) if next measured value was read rd.t1 < rd.t2 if end of data reached rd.t1 = rd.t2 *) PROCEDURE (rd: Reader) ReadPrev*, NEW, ABSTRACT; (* Read previous measured value. Postcondition: rd.f1 - the value measured at time t1 rd.f2 - the value measured at time t2 if Read called for the first time after carrier creation rd.t1 = 0 if Read called not for the first time and rd.t1’ # 0 rd.t2 = rd.t1' (fn: t1’ - previous t1) if the beginning of measurements reached rd.t1 = 0 *) (* Directory *) PROCEDURE (d: Directory) New* (f0: REAL): Model, NEW, ABSTRACT; (* Create new model for data accumulation: f0 - the value at the initial (zero) time *) (* StdModel *) PROCEDURE NewPage (tLast, dt: INTEGER; f: REAL): Page; VAR p: Page; BEGIN NEW(p); p.t[0] := tLast + dt; p.f[0] := f; p.n := 1; RETURN p END NewPage; PROCEDURE AddToPage (p: Page; dt: INTEGER; f: REAL); BEGIN p.t[p.n] := p.t[p.n-1] + dt; p.f[p.n] := f; INC(p.n) END AddToPage; PROCEDURE (m: StdModel) AddSample (dt: INTEGER; f: REAL); VAR msg: Models.UpdateMsg; BEGIN ASSERT(dt > 0, 20); IF m.last.n < pageSize THEN AddToPage(m.last, dt, f) ELSE m.last.next := NewPage(m.last.t[pageSize-1], dt, f); m.last.next.prev := m.last; m.last := m.last.next END; INC(m.n); Models.Broadcast(m, msg) END AddSample; PROCEDURE (m: StdModel) Length (): INTEGER; BEGIN IF m.last # NIL THEN RETURN m.last.t[m.last.n-1] ELSE RETURN 0 END END Length; PROCEDURE (m: StdModel) NewReader (): Reader; VAR rd: StdReader; BEGIN ASSERT(m.first # NIL, 20); NEW(rd); rd.base := m; RETURN rd END NewReader; (* Model loading, saving and copying *) PROCEDURE (m: StdModel) Externalize (VAR wr: Stores.Writer); VAR p: Page; i: INTEGER; n: INTEGER; BEGIN wr.WriteVersion(version); wr.WriteInt(m.n); p := m.first; n := 0; WHILE p # NIL DO i := 0; FOR i := 0 TO p.n-1 DO wr.WriteInt(p.t[i]); wr.WriteReal(p.f[i]); INC(n) END; p := p.next END END Externalize; PROCEDURE (m: StdModel) Internalize (VAR rd: Stores.Reader); VAR ver, n, i, t0, t: INTEGER; f: REAL; BEGIN rd.ReadVersion(0, version, ver); IF ~rd.cancelled THEN rd.ReadInt(n); rd.ReadInt(t); rd.ReadReal(f); m.n := 1; NEW(m.first); m.last := m.first; m.first.f[0] := f; m.first.n := 1; t0 := 0; FOR i := 1 TO n-1 DO rd.ReadInt(t); rd.ReadReal(f); m.AddSample(t - t0, f); t0 := t END END END Internalize; PROCEDURE (m: StdModel) CopyFrom (source: Stores.Store); VAR p: Page; i: INTEGER; BEGIN WITH source: StdModel DO p := source.first; NEW(m.first); m.last := m.first; WHILE p # NIL DO FOR i := 0 TO p.n-1 DO m.last.t[i] := p.t[i]; m.last.f[i] := p.f[i] END; m.last.n := p.n; p := p.next; IF p # NIL THEN NEW(m.last.next); m.last := m.last.next END END; m.n := source.n END END CopyFrom; (* StdReader *) PROCEDURE (rd: StdReader) Base (): Model; BEGIN RETURN rd.base END Base; PROCEDURE LookAtPage (p: Page; t: INTEGER): INTEGER; VAR l, r, c: INTEGER; BEGIN (* Pre p.t[0] <= t <= p.t[p.n-1] *) l := 0; r := p.n-1; WHILE l < r-1 DO c := (r+l) DIV 2; IF p.t[c] <= t THEN l := c ELSE (* t < p.t[c] *) r := c END END; (* Post (p.n > 1) & (l = r-1) & (p.t[l] <= t) & (t < p.t[r]) OR (p.n = 1) & (l = r) & (p.t[l] <= t) *) RETURN l END LookAtPage; PROCEDURE ExtractIt (rd: StdReader); BEGIN rd.t1 := rd.page.t[rd.n]; rd.f1 := rd.page.f[rd.n]; IF rd.n + 1 < rd.page.n THEN rd.t2 := rd.page.t[rd.n+1]; rd.f2 := rd.page.f[rd.n+1] ELSIF rd.page.next # NIL THEN rd.t2 := rd.page.next.t[0]; rd.f2 := rd.page.next.f[0] ELSE rd.t2 := rd.t1; rd.f2 := rd.f1 END END ExtractIt; PROCEDURE (rd: StdReader) SetPos (t: INTEGER); VAR p1, p2: Page; BEGIN ASSERT((t >= 0), 20); p2 := rd.base.first; WHILE (p2 # NIL) & (p2.t[p2.n-1] < t) DO p1 := p2; p2 := p2.next END; (* Post: (p1 = NIL) OR (p1.t[last] < t), (p2 = NIL) OR (t <= p2.t[last]), (p1 # NIL) OR (p2 # NIL) ==> Post: (p1 = NIL) & (p2 # NIL) & (t <= p2.t[last]) OR (p1 # NIL) & (p2 # NIL) & (p1.t[last] < t) & (t <= p2.t[last]) OR (p1 # NIL) & (p2 = NIL) & (p1.t[last] < t) *) IF p1 = NIL THEN rd.page := p2; rd.n := LookAtPage(p2, t); ELSIF p2 # NIL THEN IF t >= p2.t[p2.n-1] THEN rd.page := p2; rd.n := LookAtPage(p2, t) ELSE rd.page := p1; rd.n := pageSize-1 END ELSE rd.page := p1; rd.n := pageSize END; ExtractIt(rd) END SetPos; PROCEDURE (rd: StdReader) Read; BEGIN IF rd.page # NIL THEN IF rd.n + 1 < rd.page.n THEN INC(rd.n) ELSIF rd.page.next # NIL THEN rd.page := rd.page.next; rd.n := 0 END; ELSE rd.page := rd.base.first; rd.n := 0 END; ExtractIt(rd) END Read; PROCEDURE (rd: StdReader) ReadPrev; BEGIN IF rd.page # NIL THEN IF rd.n > 0 THEN DEC(rd.n) ELSIF rd.page.prev # NIL THEN rd.page := rd.page.prev; rd.n := pageSize END; ELSE rd.page := rd.base.first; rd.n := 0 END; ExtractIt(rd) END ReadPrev; (* StdDirectory *) PROCEDURE (d: StdDirectory) New (f0: REAL): StdModel; VAR m: StdModel; BEGIN NEW(m); m.first := NewPage(0, 0, f0); m.last := m.first; m.n := 1; RETURN m END New; PROCEDURE SetDir* (d: Directory); BEGIN ASSERT(d # NIL, 20); dir := d END SetDir; (* Interactive data acquisition *) PROCEDURE OpenSensor (s: Sensor); VAR val: RECORD (Meta.Value) proc: SensorProc END; ok: BOOLEAN; BEGIN s.adr := NIL; Meta.LookupPath(s.proc, s.it); IF s.it.Valid() & (s.it.obj = Meta.procObj) THEN s.it.GetVal(val, ok); IF ok THEN s.adr := val.proc END END END OpenSensor; PROCEDURE (s: Sensor) Do; VAR f: REAL; t: LONGINT; BEGIN IF ~s.it.Valid() THEN (* module was unloaded *) OpenSensor(s) END; IF s.adr # NIL THEN f := s.adr(s.num); t := Services.Ticks(); s.m.AddSample(MAX(1, SHORT(ENTIER((t - s.lastT) * s.tRatio))), f * s.fRatio); s.lastT := t END; Services.DoLater(s, t + s.interval) END Do; PROCEDURE Connect* (IN sensorProc: ARRAY OF CHAR; sensorNum: INTEGER; buffer: Model; interval: INTEGER; tRatio, fRatio: REAL); VAR s: Sensor; BEGIN ASSERT(buffer.sensor = NIL, 20); NEW(s); s.m := buffer; buffer.sensor := s; s.proc := sensorProc$; s.num := sensorNum; s.interval := interval; s.tRatio := tRatio; s.fRatio := fRatio; s.lastT := Services.Ticks(); OpenSensor(s); IF s.adr # NIL THEN Services.DoLater(s, Services.now) END END Connect; PROCEDURE Disconnect* (buffer: Model); BEGIN ASSERT(buffer.sensor # NIL, 20); Services.RemoveAction(buffer.sensor); buffer.sensor := NIL END Disconnect; PROCEDURE Init; VAR d: StdDirectory; BEGIN NEW(d); stdDir := d; dir := d END Init; BEGIN Init END MyHistoryModels.
Testing our model
Good style - to compile, and run your creation as early as possible. However, it is efficient only when combined with the method of stepwise refinement and clarification (see. Niklaus Wirth "Systematic Programming"). Development “at random” does not give anything worthwhile (see. Informatics-21, About programming discipline). Iterative development, with short working versions cycle, is used in well-known today development process, as Extreme Programming, and others so-called «flexible approaches.»
Let’s write the module MyHistoryTest:
MODULE MyHistoryTest; IMPORT HistoryModels := MyHistoryModels, ObxRandom; VAR m: HistoryModels.Model; PROCEDURE Sensor* (num: INTEGER): REAL; BEGIN RETURN -1 + ObxRandom.Uniform() * 2 END Sensor; PROCEDURE Start*; BEGIN HistoryModels.Connect("MyHistoryTest.Sensor", 0, m, 10, 1, 1) END Start; PROCEDURE Stop*; BEGIN HistoryModels.Disconnect(m) END Stop; BEGIN m := HistoryModels.dir.New(0) END MyHistoryTest. ^Q MyHistoryTest.Start ^Q MyHistoryTest.Stop
Let’s try to collect data. How to see, if it really works→ Select the name of the module MyHistoryTest and execute BlackBox menu command Info → Global Variables, then navigate through the model’s data structure, making sure that everything goes right. For test purpose, you can set MyHistoryModels.pageSize constant to smaller value - to reduce page flipping time.
View development
Development of view is simple work. As usual, let’s define interface - abstract type MyHistoryViews.View. We define the most important items only (without which «the life is not sweet»). Do not forget, that everyone, who has even incredible ideas, can implement them in his own separate modules, and connect them to our model. Or even use our view internally («wrapper» with delegated functionality), by defining additional functions on top of it.
The only significant feature we consider horizontally and vertically scaling. Additional useful and free option - to choose the color of the chart line. We don’t need to define special procedure for it. It is sufficient to handle special. framework message - and you can use menu Attributes to choose the color. And of course, we will support scrolling - because it is a necessity, not the optional feature.
Finally we get this interface:
DEFINITION MyHistoryViews; IMPORT Views, MyHistoryModels; TYPE View = POINTER TO ABSTRACT RECORD (Views.View(Stores.Store)) (v: View) GetScale (OUT unitsIn1t, unitsIn1f: INTEGER), NEW, ABSTRACT; (v: View) SetScale (unitsIn1t, unitsIn1f: INTEGER), NEW, ABSTRACT END; Directory = POINTER TO ABSTRACT RECORD (d: Directory) New (m: MyHistoryModels.Model): View, NEW, ABSTRACT END; VAR dir-: Directory; stdDir-: Directory; PROCEDURE SetDir (d: Directory); END MyHistoryViews.
The scale is specified in universal units (see. Documentation module Ports, 1 mm = 36000 universal units) per unit of time (vertical) or measured value (horizontal).
Below is the view implementation with all required functionality. Explanations:
- Scrolling. Scrollbars in the framework automatically supported by windows, in which document is opened. Or, if you need to scroll embedded view, you can use standard scrollbar component (StdScrollers, menu command Tools → Add Scroller). The view may support scrolling in two ways. The simplest one - just to set a bigger size. Nothing else required. The scroll bars appear automatically. But it is good only for views, which are not too big (two to three pages maximum). View that works with bigger data volume, should clearly know the page size, scroll position of the displayed data, and their total length. View pass this information to framework by handling Controllers.PollSectionMsg message in procedure HandleCtrlMsg. Scrollbars, no matter what provides them, will show the current status. To respond on scroll events by changing its state, the view should handle the message Controllers.ScrollMsg and notify framework, that it adjusts its size to given, by handling Properties.ResizePrefwindow message in procedure HandlePropMsg.
- The procedure HandleModelMsg handles the message about the changes in the model - for the timely repaint.
MODULE MyHistoryViews; IMPORT HistoryModels := MyHistoryModels, Views, Stores, Ports, Controllers, Models, Properties, Fonts; CONST version = 0; TYPE View* = POINTER TO ABSTRACT RECORD (Views.View) END; Directory* = POINTER TO ABSTRACT RECORD END; StdView = POINTER TO RECORD (View) m: HistoryModels.Model; ut, uf: INTEGER; plotColor: INTEGER; rd: HistoryModels.Reader; topT: INTEGER END; StdDirectory = POINTER TO RECORD (Directory) END; VAR dir-, stdDir-: Directory; PROCEDURE (v: View) SetScale* (unitsIn1t, unitsIn1f: INTEGER), NEW, ABSTRACT; PROCEDURE (v: View) GetScale* (OUT unitsIn1t, unitsIn1f: INTEGER), NEW, ABSTRACT; PROCEDURE (d: Directory) New* (m: HistoryModels.Model): View, NEW, ABSTRACT; PROCEDURE InitV (v: StdView); BEGIN v.rd := v.m.NewReader() END InitV; PROCEDURE (v: StdView) SetScale (unitsIn1t, unitsIn1f: INTEGER); BEGIN ASSERT((unitsIn1t > 0) & (unitsIn1f > 0), 20); v.ut := unitsIn1t; v.uf := unitsIn1f; Views.Update(v, Views.keepFrames) END SetScale; PROCEDURE (v: StdView) GetScale (OUT unitsIn1t, unitsIn1f: INTEGER); BEGIN unitsIn1t := v.ut; unitsIn1f := v.uf; END GetScale; PROCEDURE (v: StdView) HandlePropMsg (VAR msg: Properties.Message); VAR stdProp: Properties.StdProp; prop: Properties.Property; BEGIN WITH msg: Properties.ResizePref DO msg.fixed := FALSE; msg.verFitToWin := TRUE | msg: Properties.PollMsg DO NEW(stdProp); stdProp.color.val := v.plotColor; stdProp.valid := {Properties.color}; stdProp.known := {Properties.color}; Properties.Insert(msg.prop, stdProp) | msg: Properties.SetMsg DO prop := msg.prop; WHILE prop # NIL DO WITH prop: Properties.StdProp DO IF Properties.color IN prop.valid THEN v.plotColor := prop.color.val END ELSE END; prop := prop.next END; Views.Update(v, Views.keepFrames) ELSE END END HandlePropMsg; PROCEDURE DrawPlot (f: Views.Frame; w, h: INTEGER; v: StdView; topT, bottomT: INTEGER; color: INTEGER); BEGIN v.rd.SetPos(topT); REPEAT f.DrawLine(w DIV 2 + SHORT(ENTIER(v.rd.f1*v.uf)), (v.rd.t1 - topT) * v.ut, w DIV 2 + SHORT(ENTIER(v.rd.f2*v.uf)), (v.rd.t2 - topT) * v.ut, 0, color); v.rd.Read UNTIL (v.rd.t1 > bottomT) OR (v.rd.t1 = v.rd.t2) END DrawPlot; PROCEDURE DrawBack (f: Views.Frame; w, h: INTEGER); VAR font: Fonts.Font; BEGIN f.DrawLine(SHORT(ENTIER(w / 2)), 0, SHORT(ENTIER(w / 2)), h, 0, Ports.black); font := Fonts.dir.Default(); f.DrawString(0, font.size, Ports.black, "Press + / - for vertical scaling", font); f.DrawString(0, 2*font.size, Ports.black, "or w / s for horizontal scaling.", font); END DrawBack; PROCEDURE (v: StdView) Restore (f: Views.Frame; l, t, r, b: INTEGER); VAR w, h, topT, bottomT, page: INTEGER; BEGIN v.context.GetSize(w, h); page := SHORT(ENTIER(h / v.ut)); DrawBack(f, w, h); topT := v.topT; bottomT := v.topT + SHORT(ENTIER(h / v.ut)); INC(topT, MAX(0, SHORT(ENTIER(page * t / h)-1))); DEC(bottomT, MAX(0, SHORT(ENTIER(page * (h - b) / h)-1))); DrawPlot(f, w, h, v, topT, bottomT, v.plotColor) END Restore; PROCEDURE (v: StdView) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message; VAR focus: Views.View); VAR w, h, page: INTEGER; BEGIN v.context.GetSize(w, h); page := SHORT(ENTIER(h / v.ut)); WITH msg: Controllers.PollSectionMsg DO IF msg.vertical THEN IF v.m.Length() > page THEN msg.wholeSize := v.m.Length(); msg.partPos := v.topT; msg.partSize := SHORT(ENTIER(h / v.ut)); msg.valid := TRUE; END; msg.done := TRUE END | msg: Controllers.ScrollMsg DO IF msg.vertical THEN CASE msg.op OF | Controllers.decLine: v.topT := MAX(0, v.topT - page DIV 10 -1) | Controllers.incLine: v.topT := MIN(v.topT + page DIV 10 + 1, v.m.Length()) | Controllers.decPage: v.topT := MAX(0, v.topT - page) | Controllers.incPage: v.topT := MIN(v.topT + page, v.m.Length()) | Controllers.gotoPos: v.topT := MAX(0, MIN(msg.pos, v.m.Length())) END; msg.done := TRUE; Views.Update(v, Views.keepFrames) END; | msg: Controllers.EditMsg DO IF msg.op = Controllers.pasteChar THEN CASE msg.char OF | '+': v.SetScale(SHORT(ENTIER(v.ut * 1.5))+1, v.uf) | '-': v.SetScale(MAX(1, SHORT(ENTIER(v.ut / 1.5))), v.uf) | 'w', 'W': v.SetScale(v.ut, SHORT(ENTIER(v.uf * 1.5))+1) | 's', 'S': v.SetScale(v.ut, MAX(1, SHORT(ENTIER(v.uf / 1.5)))) ELSE END; Views.Update(v, Views.keepFrames) END ELSE END END HandleCtrlMsg; PROCEDURE (v: StdView) HandleModelMsg (VAR msg: Models.Message); BEGIN WITH msg: Models.UpdateMsg DO Views.Update(v, Views.keepFrames) END END HandleModelMsg; PROCEDURE (v: StdView) Externalize (VAR wr: Stores.Writer); BEGIN wr.WriteVersion(version); wr.WriteStore(v.m); wr.WriteInt(v.ut); wr.WriteInt(v.uf) END Externalize; PROCEDURE (v: StdView) Internalize (VAR rd: Stores.Reader); VAR ver: INTEGER; m: Stores.Store; BEGIN rd.ReadVersion(0, version, ver); IF ~rd.cancelled THEN rd.ReadStore(m); IF ~(m IS HistoryModels.Model) THEN rd.TurnIntoAlien(Stores.alienComponent); RETURN END; v.m := m(HistoryModels.Model); rd.ReadInt(v.ut); rd.ReadInt(v.uf); InitV(v) END END Internalize; PROCEDURE (v: StdView) ThisModel (): HistoryModels.Model; BEGIN RETURN v.m END ThisModel; PROCEDURE (v: StdView) CopyFromModelView (source: Views.View; model: Models.Model); BEGIN WITH source: StdView DO v.m := model(HistoryModels.Model); v.ut := source.ut; v.uf := source.uf; v.plotColor := source.plotColor; InitV(v); v.topT := source.topT END END CopyFromModelView; PROCEDURE (d: StdDirectory) New (m: HistoryModels.Model): StdView; VAR v: StdView; BEGIN ASSERT(m # NIL, 20); NEW(v); v.m := m; Stores.Join(v, m); v.ut := 360; v.uf := 50 * 36000; v.plotColor := Ports.red; InitV(v); RETURN v END New; PROCEDURE SetDir* (d: Directory); BEGIN ASSERT(d # NIL, 20); dir := d END SetDir; PROCEDURE Init; VAR d: StdDirectory; BEGIN NEW(d); stdDir := d; dir := d END Init; BEGIN Init END MyHistoryViews.
Final test
Finally the module MyHistoryTest looks as follow:
MODULE MyHistoryTest; IMPORT HistoryModels := MyHistoryModels, ObxRandom, HistoryViews := MyHistoryViews, Views, StdCmds; VAR m: HistoryModels.Model; PROCEDURE Sensor* (num: INTEGER): REAL; BEGIN RETURN -1 + ObxRandom.Uniform() * 2 END Sensor; PROCEDURE Start*; BEGIN HistoryModels.Connect("MyHistoryTest.Sensor", 0, m, 10, 1, 1) END Start; PROCEDURE Stop*; BEGIN HistoryModels.Disconnect(m) END Stop; PROCEDURE Open*; VAR v: HistoryViews.View; BEGIN v := HistoryViews.dir.New(m); Views.Deposit(v); StdCmds.Open END Open; BEGIN m := HistoryModels.dir.New(0) END MyHistoryTest. ^Q MyHistoryTest.Start ^Q MyHistoryTest.Stop ^Q MyHistoryTest.Open
Start the data collection and open the view - you'll see a running graph. By +/- keys you can change the vertical scale, by w/s keys - horizontally one. To change the graph color, you have to select the entire view (see. Manuals and documentation of framework) - for example, Edit → Select Document (Ctrl + Space), and then specify the color in Attributes menu.
Let's play with our view. For example, press F2, when its window is opened. Another window with the same data will appear. You can independently scroll and change the scale and color in it. You can open any number of windows. Please note, that we had to write almost nothing for it. It was sufficient just to implement procedure CopyFromModelView. Framework creates new same type view, and calls this procedure to pass the same model to it. This way several views show the same model.
And by highlighting our view (see. the paragraph above) and pressing Ctrl-C (Edit → Copy), we will make complete copy of view with the model (Framework calls the same method, but copies the model first). Then the view can be inserted anywhere - in text, form, etc. But we have to wrap it by scroller before (Tools → Add Scroller).
And, of course, you can save and load the graph - as its own document or as embedded in other ones.