The exercise of writing out requirements and mulling them over yielded the desired insights about how to proceed. Today I’ll describe the external display mechanism, which blindly redraws all of the desired values with every global redraw event and in a manner entirely external to the simulation element.
Here’s the code for the individual display items. The addition here is the updateValue
method, which takes the new input value as a parameter. Remembering that JavaScript lets you do some interesting things–while also being highly annoying and cumbersome–we see that the method is capable of handling inputs that are functions. If the input parameter is not a function then the parameter is assumed to be a value which can be assigned directly. If the input parameter is a function (that itself has no parameters, we can expand that capability later), then the method obtains the value returned by the function. Naturally this only works if the function passed as a parameter returns a usable value. This method works for simple functions but does not appear to work if you pass in a method call to an object. That is apparently too many layers of indirection. I have it as a note to explore this further.
The other thing the update does is check to see if the new value is different than the last value. If it is the method returns a value of true (as opposed to a default of false). This capability isn’t important for today’s work but will be for tomorrow’s.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
function DisplayValue(value,label,format,places) { //assumes alignment and color/fillStyle are set before calling this.value = value; this.label = label + ":"; this.format = format; this.places = places; globalCTX.font = "12px Arial"; this.width = globalCTX.measureText(label).width; this.getWidth = function() { return this.width; }; //here's the new capability this.updateValue = function(newValue) { var tempValue = ''; if (typeof newValue === "function") { tempValue = newValue.call(tempValue); //TODO: expand to handle optional parameters } else { tempValue = newValue; } if (tempValue != this.value) { this.value = tempValue; return true; } else { return false; } }; this.drawValue = function(x,y) { var s; if (this.format == "integer") { s = this.value; } else if (this.format == "numdec") { if (typeof this.value === "number") { s = this.value.toFixed(this.places); } else { s = this.value; } } else if (this.format == "numwide") { if (typeof this.value === "number") { s = this.value.toPrecision(this.places); } else { s = this.value; } } else if (this.format == "text") { s = this.value; } else if (this.format == "bool") { if (typeof this.value === "boolean") { if (this.value) { s = "TRUE"; } else { s = "FALSE"; } } else { s = this.value; } } globalCTX.fillText(s,x,y); } } //displayValue |
Next up is a reworking of the DisplayGroup
object. Since the mechanism must be completely external to any simulation element I went ahead and moved the definition of many of the parameters back to the function/constructor call itself (the define
method is left in place for now). I also made the addValue
method capable of accepting functions as arguments, with the same limitations described above. I also modified the object so that an empty string can be passed in for the label value, in which case the values will be drawn starting on the first line instead of the second.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
function DisplayGroup(label,x,y,vw,lc,vc,bc) { this.label = label; this.xLocation = x; this.yLocation = y; this.valueWidth = vw; this.labelColor = lc; this.valueColor = vc; this.borderColor = bc; if (this.label.length > 0) { this.labelHeight = 12; } else { this.labelHeight = 0; } this.maxLabelWidth = 0; this.valueCount = 0; this.valueList = []; //new Array(); this.define = function(label,x,y,vw,lc,vc,bc) { //used to define outside of an object this.label = label; this.xLocation = x; this.yLocation = y; this.valueWidth = vw; this.labelColor = lc; this.valueColor = vc; this.borderColor = bc; if (this.label.length > 0) { this.labelHeight = 12; } else { this.labelHeight = 0; } }; this.addValue = function(value,label,format,places) { var tempValue = ''; if (typeof value === "function") { tempValue = value.call(tempValue); } else { tempValue = value; } var v = new DisplayValue(tempValue,label,format,places); this.valueList.push(v); this.valueCount++; globalCTX.font = "12px Arial"; var w = v.getWidth(); if (w > this.maxLabelWidth) { this.maxLabelWidth = w; } }; this.drawBasic = function() { this.height = (this.valueCount * 12) + this.labelHeight + 3; this.width = this.maxLabelWidth + this.valueWidth + 8; globalCTX.strokeStyle = this.borderColor; globalCTX.beginPath(); globalCTX.moveTo(this.xLocation+0.5,this.yLocation+0.5); globalCTX.lineTo(this.xLocation+this.width+0.5,this.yLocation+0.5); globalCTX.lineTo(this.xLocation+this.width+0.5,this.yLocation+this.height+0.5); globalCTX.lineTo(this.xLocation+0.5,this.yLocation+this.height+0.5); globalCTX.lineTo(this.xLocation+0.5,this.yLocation+0.5); globalCTX.stroke(); globalCTX.font = "12px Arial"; globalCTX.fillStyle = this.labelColor; if (this.label.length > 0) { globalCTX.textAlign = "center"; globalCTX.fillText(this.label,this.xLocation+(this.width*0.5),this.yLocation+12); } globalCTX.textAlign = "right"; for (var i=0; i<this.valueCount; i++) { var l = this.valueList[i].label; var x = this.xLocation+this.maxLabelWidth+5; var y = this.yLocation+(i*12)+this.labelHeight+12; //globalCTX.fillText(valueList[i].label,this.xLocation+this.maxLabelWidth+3,this.yLocation+(i*12)+12); globalCTX.fillText(l,x,y); } globalCTX.fillStyle = this.valueColor; globalCTX.textAlign = "left"; for (i=0; i<this.valueCount; i++) { this.valueList[i].drawValue(this.xLocation+this.maxLabelWidth+8,this.yLocation+(i*12)+this.labelHeight+12); } }; } //DisplayGroup |
Here is the streamlined code for the Queue component, from yesterday.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
function QueueComponent(displayDelay) { setOfComponents.push(this); this.componentID = getNewID(); this.componentName = "Queue"; this.currentEntryEntityID = ""; this.countInQueue = 0; this.exitResidenceTime = ""; this.nextComponent = null; this.entityQueue = []; this.drawData = function() { }; this.assignNextComponent = function(next) { this.nextComponent = next; }; this.forwardEntity = function() { if (this.nextComponent.isOpen()) { var entity = this.entityQueue.pop(); if (entity) { //calculate how long item was in queue this.exitResidenceTime = globalSimClock - entity.getLocalEntryTime(); //now use this to calculate stats for the interval //TODO: calculate stats as needed this.countInQueue--; this.currentExitEntityID = entity.entityID; displayProgressText("Queue comp. "+this.componentID+" forwards entity: "+this.currentExitEntityID+" at time "+globalSimClock.toFixed(6)); this.nextComponent.receiveEntity(entity); } } }; this.receiveEntity = function(entity) { //receive the entity entity.setLocalEntryTime(); //record time entity entered queue this.entityQueue.unshift(entity); this.countInQueue++; //display what was done this.currentEntryEntityID = entity.entityID; displayProgressText("Queue comp. "+this.componentID+" receives entity: "+this.currentEntryEntityID+" at time "+globalSimClock.toFixed(6)); if (this.nextComponent.isOpen()) { this.forwardEntity(); } }; } //QueueComponent |
Here is how a queue component and all of the display mechanisms are instantiated.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//declare the Queue component itself var queue1 = new QueueComponent(); //declare the external display object var queue1DataGroup = new DisplayGroup("Queue",175,79,80,"#00FFFF","#FF0000","#FFFF00",0.0); //set up the values to be displayed within var gtemp = queue1.getComponentID(); queue1DataGroup.addValue(gtemp,"Comp. ID","integer"); gtemp = queue1.getCountInQueue(); queue1DataGroup.addValue(gtemp,"# In Queue","integer"); //create an external custom update function //this is where JavaScript gets annoying function queue1DataGroupUpdate() { var temp = queue1.getComponentID(); queue1DataGroup.valueList[0].updateValue(temp); temp = queue1.getCountInQueue(); queue1DataGroup.valueList[1].updateValue(temp); }; |
Finally, the requisite calls to update and draw the values are made for every screen refresh. In theory these calls can be added to a global array like the components are, which would allow them to also be invoked in a loop.
1 2 3 4 5 6 7 8 |
//draw everything clearCanvas("#000000"); var numComponents = setOfComponents.length; for (i=0; i<numComponents; i++) { setOfComponents[i].drawData(); } queue1DataGroupUpdate(); queue1DataGroup.drawBasic(); |
As described previously, much of this overhead is due to the fact that JavaScript enforces passing simple data types by value, which means we have to go out of our way to provide updates. Beyond that we have to define everything about the external display mechanisms before using them.
Tomorrow I’ll expand these definitions to support display groups that are visible only for a limited time when triggered by a change in the displayed values.