Ермаков И. Е. График истории показаний. 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.

Library, Ermakov I.E., Khaliullin N., BB, Education

© 2005-2024 OberonCore и коллектив авторов.