/** Protein renderer that parses rendering info from the Biochemfusion protein cartridge Proteax and produces a graphical output. (C) 2008, 2009 Jan Holst Jensen, jan@biochemfusion.com. All rights reserved. This Flash action script code file (ProteinRenderer.as) is released under a BSD-style license: * Copyright (C) 2008, 2009 Biochemfusion (http://www.biochemfusion.com) * All rights reserved. * * Redistribution and use for any purpose in source and binary forms, with or * without modification, are permitted, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. * * THIS SOFTWARE IS PROVIDED BY Biochemfusion ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL Biochemfusion BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ package { import flash.display.DisplayObjectContainer; import flash.display.Graphics; import flash.display.Shape; import flash.text.*; import flash.events.*; import flash.display.StageAlign; import flash.display.StageScaleMode; import BCFScrollBar; // Almost direct translaion of the corresponding Delphi class for protein rendering (ProteinRenderer.pas). // Probably not a very optimal implementation (notably the profusion of TextField instances), but it works sufficiently well. public class ProteinRenderer { // Info passed to main public function. private var canvasContainer:DisplayObjectContainer; private var aCanvas:Graphics; private var renderInfo:String; // Header bounding box info. private var cellSize:int; private var lineSpacing:int; private var lettersPerLine:int; private var numRows:int; private var residueCount:int; // Resulting bounding box size in pixels (when un-scaled). private var xBBoxSize:int; private var yBBoxSize:int; // Scaling virtual coordinates to screen pixels. private var xScale:Number; private var yScale:Number; private var xOffset:int; // Default font and its metrics for all text output via textOut(). private var fontFormat:TextFormat; private var fontMetrics:TextLineMetrics; // For parsing render info input. private var fromPos:int; private var toPos:int; // Emulation of in-out parameter in RenderChain(). private var residueIndex:int; private var scrollBar:BCFScrollBar; // Maximum number of digits in numbering. private static const MAX_NUM_DIGITS:int = 5; private static const PAD_STRING:String = " "; private static const NUM_CHAIN_BACKGND_COLORS:int = 2; private static const CHAIN_BACKGND_COLORS:Array = [0xC0DCC0, 0xA6CAF0]; // clMoneyGreen = RGB(192, 220, 192), clSkyBlue = RGB(166, 202, 240). private static const NUM_CHAIN_HILITE_COLORS:int = 2; private static const CHAIN_HILITE_COLORS:Array = [0x00FF00, 0x00FFFF]; // clLime = RGB(0, 255, 0), clAqua = RGB(0, 255, 255). private static const NUM_DISULFIDE_COLORS:int = 3; private static const DISULFIDE_COLORS:Array = [0xFF0000, 0xFF6400, 0xFF0064]; private static const NUM_CYCLE_COLORS:int = 3; private static const CYCLE_COLORS:Array = [0x00DC00, 0x96DC00, 0x00DC96]; // Emulate Delphi's TCanvas.TextOut(). private function textOut(x:int, y:int, textCaption:String):void { var txt:TextField = new TextField(); txt.defaultTextFormat = fontFormat; txt.text = textCaption; txt.x = x; txt.y = y - (fontMetrics.height / 10); txt.width = fontMetrics.width * (textCaption.length + 2); // Needs to set height too, the default height apparently covers more than one line (or is undefined ?) // and results in a confusing reported height of the TextField's container. txt.height = fontMetrics.height + 1; canvasContainer.addChild(txt); } private function check(condition:Boolean, errMsg:String):void { if (!condition) throw Error(errMsg); } /* Parser helper functions. */ private function nextDelim(delims:String, mustExist:Boolean):void { while ( (toPos <= renderInfo.length) && (delims.indexOf(renderInfo.charAt(toPos)) < 0) ) toPos++; if (mustExist) check(toPos <= renderInfo.length, 'Missing "' + delims + '".'); } private function nextInt():int { var result:int; toPos = fromPos; nextDelim(RenderTags.SEQ_RNDR_DELIM, true); check(toPos != fromPos, 'Missing value.'); result = parseInt(renderInfo.substr(fromPos, toPos - fromPos)); fromPos = toPos + 1; return result; } private function lastInt():int { var result:int; toPos = fromPos; nextDelim(RenderTags.SEQ_RNDR_TAG_START, true); check(toPos != fromPos, 'Missing value.'); result = parseInt(renderInfo.substr(fromPos, toPos - fromPos)); fromPos = toPos; return result; } private function nextOrLastInt():int { var result:int; toPos = fromPos; nextDelim(RenderTags.SEQ_RNDR_DELIM + RenderTags.SEQ_RNDR_TAG_START, true); check(toPos != fromPos, 'Missing value.'); result = parseInt(renderInfo.substr(fromPos, toPos - fromPos)); fromPos = toPos; if ( renderInfo.charAt(fromPos) == RenderTags.SEQ_RNDR_DELIM ) fromPos++; return result; } /* Coordinate conversion functions. */ private function xToPixels(x:int):int { return Math.round(x * xScale); } private function yToPixels(y:int):int { return Math.round(y * yScale); } private function xToScreen(x:int):int { return xToPixels(x) + xOffset; } private function yToScreen(y:int):int { return yToPixels(y) + yToPixels(lineSpacing); } private function readHeader():void { // Valid render info at all ? check( renderInfo.substr(0, RenderTags.SEQ_RNDR_TAG_INFO_BEGIN.length) == RenderTags.SEQ_RNDR_TAG_INFO_BEGIN, 'Rendering info must start with header "' + RenderTags.SEQ_RNDR_TAG_INFO_BEGIN + '".' ); check( renderInfo.substr(RenderTags.SEQ_RNDR_TAG_INFO_BEGIN.length, 3) == RenderTags.SEQ_RNDR_TAG_BOUNDBOX, 'Rendering header must be followed by a bounding box "' + RenderTags.SEQ_RNDR_TAG_BOUNDBOX + '".' ); check( renderInfo.substr(renderInfo.length - 3, 3) == RenderTags.SEQ_RNDR_TAG_INFO_END, 'Rendering info must end with an End-of-Data tag "' + RenderTags.SEQ_RNDR_TAG_INFO_END + '".' ); // Read bounding box values. fromPos = RenderTags.SEQ_RNDR_TAG_INFO_BEGIN.length + RenderTags.SEQ_RNDR_TAG_BOUNDBOX.length; try { cellSize = nextInt(); lineSpacing = nextInt(); lettersPerLine = nextInt(); numRows = nextInt(); residueCount = lastInt(); } catch (e:Error) { throw Error('Error reading bounding box: ' + e); } } // Render a white background to render sequence on. private function renderFrame():void { xBBoxSize = xToScreen(cellSize * lettersPerLine); yBBoxSize = yToScreen((cellSize + lineSpacing) * numRows); // Add some breathing space. xBBoxSize = xBBoxSize + xToPixels(cellSize * 2); yBBoxSize = yBBoxSize + yToPixels(lineSpacing * 2); aCanvas.clear(); aCanvas.lineStyle(1, 0x000000); aCanvas.beginFill(0xFFFFFF); aCanvas.drawRect(0, 0, xBBoxSize, yBBoxSize); aCanvas.endFill(); /** // Following to ease graphic placement debugging - draw a bounding box around sequence. var xSeqSize:int = xToPixels(cellSize * lettersPerLine); var ySeqSize:int = yToPixels((cellSize + lineSpacing) * numRows); aCanvas.drawRect(xToScreen(0), yToScreen(0), xSeqSize, ySeqSize); **/ } private function renderNumbers():void { for (var i:int = 0; i < numRows; i++) { // I can't find an equivalent of Format() or sprintf() in ActionScript so we'll just pad the strings ourselves. var outStr:String = String(i * lettersPerLine + 1); outStr = PAD_STRING.substr(0, MAX_NUM_DIGITS - outStr.length) + outStr; textOut( xToScreen((-MAX_NUM_DIGITS + 1) * cellSize), yToScreen(i * (cellSize + lineSpacing)), outStr ); } } private function resPosX(resIdx:int, xOffset:int):int { return xToScreen( (resIdx % lettersPerLine) * cellSize + xOffset ); } private function resPosY(resIdx:int):int { var tempIntDivResult:int = (resIdx / lettersPerLine); return yToScreen(tempIntDivResult * (cellSize + lineSpacing)); } private function renderTermModMarks(colorIdx:int, termModBits:int, fromResidueIndex:int, toResidueIndex:int):void { var x:int = 0; var y:int = 0; aCanvas.lineStyle(1, 0x808080); //(** How do we make Pen.Style := psDot ? var rectWidth:int = Math.max(xToPixels(cellSize) / 4, 2); if ((termModBits & 1) != 0) { x = resPosX(fromResidueIndex, 0); y = resPosY(fromResidueIndex); aCanvas.beginFill(CHAIN_HILITE_COLORS[colorIdx % NUM_CHAIN_HILITE_COLORS]); aCanvas.drawRect(x, y, rectWidth, yToPixels(cellSize)); aCanvas.endFill(); } if ((termModBits & 2) != 0) { x = resPosX(toResidueIndex, cellSize); y = resPosY(toResidueIndex); aCanvas.beginFill(CHAIN_HILITE_COLORS[colorIdx % NUM_CHAIN_HILITE_COLORS]); // -1 to the left because of line width of gray line around hilite rectangle. aCanvas.drawRect(x - 1, y, -rectWidth, yToPixels(cellSize)); aCanvas.endFill(); } } private function renderChainBackground(colorIdx:int, termModBits:int, fromResidueIndex:int, toResidueIndex:int):void { aCanvas.lineStyle(0, CHAIN_BACKGND_COLORS[colorIdx % NUM_CHAIN_BACKGND_COLORS]); aCanvas.beginFill(CHAIN_BACKGND_COLORS[colorIdx % NUM_CHAIN_BACKGND_COLORS]); var rectHeight:int = yToPixels(cellSize); var savedFromIdx:int = fromResidueIndex; var savedToIdx:int = toResidueIndex; while (fromResidueIndex <= toResidueIndex) { var colFrom:int = fromResidueIndex % lettersPerLine; var colTo:int = colFrom + (toResidueIndex - fromResidueIndex); // Wrap onto next line ? if (colTo >= lettersPerLine) colTo = lettersPerLine - 1; var rectXFrom:int = xToScreen(colFrom * cellSize); var rectXTo:int = xToScreen(colTo * cellSize + cellSize); // Must do a two-step calculation here to get integer div, since there is no integer division operator in AS 3.0. var rectY:int = (fromResidueIndex / lettersPerLine); rectY = yToScreen(rectY * (cellSize + lineSpacing) ); aCanvas.drawRect(rectXFrom, rectY, rectXTo - rectXFrom, rectHeight); fromResidueIndex = fromResidueIndex + colTo - colFrom + 1; } aCanvas.endFill(); renderTermModMarks(colorIdx, termModBits, savedFromIdx, savedToIdx); } private function renderChain():void { var chainNo:int = nextInt(); nextDelim(RenderTags.SEQ_RNDR_TAG_START, true); var chain:String = renderInfo.substr(fromPos, toPos - fromPos); fromPos = toPos; check(chain.length > 3, "Empty chain"); check(chain.charAt(0) == '0' || chain.charAt(0) == '1', "Invalid N-terminal modification flag."); check(chain.charAt(1) == '0' || chain.charAt(1) == '1', "Invalid C-terminal modification flag."); check(chain.charAt(2) == RenderTags.SEQ_RNDR_DELIM, "Invalid delimiter after terminal modification flags."); var termModBits:int = 1 * parseInt(chain.charAt(0)) + 2 * parseInt(chain.charAt(1)); chain = chain.substr(3, chain.length); /* Move modification flags into a separate string. */ var modFlags:String = PAD_STRING; while (modFlags.length < chain.length) modFlags += PAD_STRING; /* ModFlags may end up being much longer than Length(Chain) if there are lots of modifications, but it won't do any harm. Rather that than re-allocating memory. */ for (var i:int = 0; i < chain.length; i++) { var mfChar:String = chain.charAt(i); if (mfChar == '1' || mfChar == '2' || mfChar == '3') { // Flash doesn't seem to have a way to replace a single char in a string, nor a String.erase() method - at least not that I could find. modFlags = modFlags.substr(0, i) + mfChar + modFlags.substr(i + 1, modFlags.length); chain = chain.substr(0, i) + chain.substr(i + 1, chain.length); } } if (chainNo != 0) renderChainBackground((chainNo - 1) % NUM_CHAIN_BACKGND_COLORS, termModBits, residueIndex, residueIndex + chain.length - 1); // Render amino acid codes. var virtX:int = (residueIndex % lettersPerLine) * cellSize; var virtY:int = (residueIndex / lettersPerLine); virtY = virtY * (cellSize + lineSpacing); for (i = 0; i < chain.length; i++) { var modFlag:int = parseInt(modFlags.charAt(i)); if ((modFlag & RenderTags.SEQ_RNDR_BITFLAG_DFORM) != 0) { var slashLine:Shape = new Shape(); slashLine.graphics.lineStyle(2, 0xFFFFFF); slashLine.graphics.moveTo(xToScreen(virtX), yToScreen(virtY + cellSize) - 2); slashLine.graphics.lineTo(xToScreen(virtX + cellSize), yToScreen(virtY) + 2); canvasContainer.addChild(slashLine); } if ((modFlag & RenderTags.SEQ_RNDR_BITFLAG_MODRES) != 0) fontFormat.color = 0xFF0000; else fontFormat.color = 0x000000; textOut(xToScreen(virtX), yToScreen(virtY), chain.charAt(i)); virtX = virtX + cellSize; if (virtX >= cellSize * lettersPerLine) { virtX = 0; virtY = virtY + cellSize + lineSpacing; } } fontFormat.color = 0x000000; residueIndex = residueIndex + chain.length; } private function renderLine(aColor:int):void { // Currently discard 'from' and 'to' information. nextInt(); nextInt(); var x:int = nextInt(); var y:int = nextInt(); var penWidth:int = Math.max(xToPixels(cellSize) / 4, 2); aCanvas.lineStyle(penWidth, aColor); aCanvas.moveTo(xToScreen(x), yToScreen(y)); // Read (X, Y) points of bridge lines. while (renderInfo.charAt(fromPos) != RenderTags.SEQ_RNDR_TAG_START) { x = nextInt(); y = nextOrLastInt(); aCanvas.lineTo(xToScreen(x), yToScreen(y)); } } // This function is useful for testing how mapping between virtual coordinates and screen coordinates works for both text and lines. private function renderTest():void { var test:String = "A234567890B234567890C234567890C234567890E2C4567890F234567890G234567890H234567890I234567890J234CC789CK2345678C0"; var stringIdx:int = 0; // Test letters. for (var row:int = 0; row < numRows; row++) { for (var col:int = 0; col < lettersPerLine; col++) { textOut( xToScreen(col * cellSize), yToScreen(row * (cellSize + lineSpacing)) - (fontMetrics.height / 10), test.charAt(stringIdx) ); stringIdx++; if (stringIdx >= test.length) break; } } // Sanity cell check. for (row = 0; row < numRows; row++) { for (col = 0; col < lettersPerLine; col++) { aCanvas.drawRect( xToScreen(col * cellSize), yToScreen(row * (cellSize + lineSpacing)), xToPixels(cellSize), yToPixels(cellSize) - 1 ); } } // Sanity background check. aCanvas.lineStyle(1, 0xFF0000); for (var rectFrom:int = 1; rectFrom <= 110; rectFrom++) { var rectHeight:int = yToPixels(cellSize); // Must do a two-step here to get integer div, since there is no integer division operator in AS 3.0. var rectY:int = ((rectFrom - 1) / lettersPerLine); rectY = yToScreen(rectY * (cellSize + lineSpacing) ); var colFrom:int = (rectFrom - 1) % lettersPerLine; var colTo:int = colFrom; var rectXFrom:int = xToScreen(colFrom * cellSize); var rectXTo:int = xToScreen(colTo * cellSize + cellSize); aCanvas.drawRect(rectXFrom, rectY, rectXTo - rectXFrom, rectHeight); } } /* Handle resizing of stage area - scale graphics to fill stage + display scroll bar or not ? */ private function stageResizeHandler(e: Event): void { var scale:Number = (canvasContainer.stage.stageWidth - scrollBar.getWidth()) / xBBoxSize; canvasContainer.scaleX = scale; canvasContainer.scaleY = scale; scrollBar.x = canvasContainer.stage.stageWidth - scrollBar.getWidth(); scrollBar.visible = (yBBoxSize * scale) > canvasContainer.stage.stageHeight; if (!scrollBar.visible) canvasContainer.y = 0; scrollBar.syncToContainer(); } public function renderProtein(the_renderInfo:String, the_target:DisplayObjectContainer, the_canvas:Graphics):void { if (the_renderInfo.length == 0) return; canvasContainer = the_target; aCanvas = the_canvas; renderInfo = the_renderInfo; fontFormat = new TextFormat(); fontFormat.font = "Courier New"; fontFormat.color = 0x000000; fontFormat.size = 16; var tempTextField:TextField = new TextField(); tempTextField.defaultTextFormat = fontFormat; tempTextField.text = "W"; fontMetrics = tempTextField.getLineMetrics(0); try { readHeader(); xScale = fontMetrics.width / cellSize * (1 + lineSpacing / cellSize); yScale = fontMetrics.height / cellSize; xOffset = fontMetrics.width * (MAX_NUM_DIGITS + 2); aCanvas.clear(); renderFrame(); renderNumbers(); residueIndex = 0; var disulfideCount:int = 0; var cycleCount:int = 0; //(** renderTest(); var tag:String = renderInfo.substr(fromPos, 3); fromPos = fromPos + 3; while (tag != RenderTags.SEQ_RNDR_TAG_INFO_END) { if (tag == RenderTags.SEQ_RNDR_TAG_CHAIN) { try { renderChain(); } catch (e:Error) { throw Error("Error rendering chain: " + e); } } else if (tag == RenderTags.SEQ_RNDR_TAG_DISULFIDE) { try { renderLine(DISULFIDE_COLORS[disulfideCount % NUM_DISULFIDE_COLORS]); disulfideCount++; } catch (e:Error) { throw Error('Error rendering disulfide bridge: ' + e); } } else if (tag == RenderTags.SEQ_RNDR_TAG_CROSSLINK) { try { // Check validity of terminal flags, but ignore them. var cycleTerminalFlags:String = renderInfo.substr(fromPos, 3); check(cycleTerminalFlags.length == 3, 'Invalid cyclization terminal flags.'); check(cycleTerminalFlags.charAt(0) == '0' || cycleTerminalFlags.charAt(0) == '1', 'Invalid cyclization terminal flag.'); check(cycleTerminalFlags.charAt(1) == '0' || cycleTerminalFlags.charAt(1) == '1', 'Invalid cyclization terminal flag.'); check(cycleTerminalFlags.charAt(2) == RenderTags.SEQ_RNDR_DELIM, 'Invalid cyclization terminal flag end delimiter.'); fromPos = fromPos + 3; renderLine(CYCLE_COLORS[cycleCount % NUM_CYCLE_COLORS]); cycleCount++; } catch (e:Error) { throw Error('Error reading cycle: ' + e); } } else throw Error('Unsupported render info tag "' + tag + '".'); // Process next tag. tag = renderInfo.substr(fromPos, 3); fromPos = fromPos + 3; } } catch (e:Error) { textOut(10, 10, String(e)); } canvasContainer.stage.scaleMode = StageScaleMode.NO_SCALE; canvasContainer.stage.align = StageAlign.TOP_LEFT; scrollBar = new BCFScrollBar(canvasContainer); scrollBar.y = 0; canvasContainer.stage.addChild(scrollBar); canvasContainer.stage.addEventListener(Event.RESIZE, stageResizeHandler); stageResizeHandler(null); } } }