package iTop { import flash.display.*; import flash.geom.*; import flash.net.*; import flash.events.*; import iTop.GraphNode; import fl.controls.Slider; import fl.events.SliderEvent; import fl.controls.Label; // The main canvas public class Navigator extends MovieClip { protected var m_oLoader:URLLoader; protected var m_aNodes:Object; protected var m_aLinks:Array; protected var m_oRootNode:GraphNode; protected var m_oCanvas:NavigatorCanvas; public var m_bChildDragging:Boolean; // Parameters protected var m_sStartPosition:String; protected var m_sDataUrl:String; protected var m_sDetailsUrl:String; protected var m_sRelation:String; protected var m_sObjClass:String; protected var m_sObjId:String; // Constants protected var m_RADIUS = 150; protected var m_Q = 0.9; // Electrostatic forces coeff protected var m_K = 1.0; // Elastic forces coeff protected var m_Kf = 0.7; // Fluid friction coeff protected var m_Ks = 30; // Solid friction coeff protected var m_deltaT = 0.1; // Interval of time between updates protected var m_MAX_ITEMS_PER_ROW = 8; protected var m_FOCUS_DELAY_COUNTDOWN = 30; // 30 images to zoom & pan correctly protected var m_fZoom:Number; // Constructor public function Navigator() { m_aLinks = new Array(); m_aNodes = new Array(); m_fZoom = 1; initParameters(); doLoadData(); addEventListener(Event.ENTER_FRAME, initGraphics); //Stop scaling the flash content stage.scaleMode = StageScaleMode.NO_SCALE; } protected function initParameters():void { m_sDataUrl = ReadParam('xmlUrl', 'http://localhost:81/pages/xml.navigator.php?operation=relation'); m_sDetailsUrl = ReadParam('drillUrl', 'http://localhost/pages/UI.php?operation=details'); m_sRelation = ReadParam('relation', 'impacts'); m_sObjClass = ReadParam('obj_class', 'Server'); m_sObjId = ReadParam('obj_id', '1'); m_sStartPosition = ReadParam('start_pos', 'left'); } function initGraphics(event:Event):void { m_oCanvas = new NavigatorCanvas(); // All drawings will occur here addChild(m_oCanvas); m_oCanvas.scaleX = m_fZoom; m_oCanvas.scaleY = m_fZoom; // Handle listeners... removeEventListener(Event.ENTER_FRAME,initGraphics); } function mouseDown(event:MouseEvent):void { trace("Click in canvas"); if (!m_bChildDragging) { m_oCanvas.startDrag(); } } function mouseReleased(event:MouseEvent):void { if (!m_bChildDragging) { m_oCanvas.stopDrag(); } } function onZoomChange(event:SliderEvent):void { SetZoomLevel(event.value/100); } function SetZoomLevel(fZoomLevel:Number):void { m_fZoom = fZoomLevel; m_oCanvas.scaleX = m_fZoom; m_oCanvas.scaleY = m_fZoom; } function GetZommLevel() { return m_fZoom; } function doLoadData() { var sSeparator:String = '?'; if (m_sDataUrl.indexOf(sSeparator) != -1) { sSeparator = '&'; } var myString:String = m_sDataUrl+sSeparator+'relation='+m_sRelation+'&class='+m_sObjClass+'&id='+m_sObjId; trace("Requesting:"+myString); var myXMLURL:URLRequest = new URLRequest(myString); m_oLoader = new URLLoader(); m_oLoader.addEventListener(Event.COMPLETE, onXMLLoadComplete); m_oLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onXMLLoadError); m_oLoader.addEventListener(IOErrorEvent.IO_ERROR, onXMLLoadError); m_oLoader.load(myXMLURL); } function onXMLLoadComplete(event:Event):void { try { var myXML:XML = XML(m_oLoader.data); //trace("Data loaded." + myXML); //trace("==========================="); parseXMLData(null, myXML, 0, 0); m_sTitle.text = myXML.attribute("title"); m_oZoomSlider.enabled = true; addEventListener(Event.ENTER_FRAME, drawLines); m_oZoomSlider.value = 100; m_oZoomSlider.addEventListener(SliderEvent.CHANGE, onZoomChange); stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown) stage.addEventListener(MouseEvent.MOUSE_UP, mouseReleased); //trace('======= Initial Posistions ========='); //DumpPositions(); } catch(error:IOErrorEvent) { m_sTitle.text = "I/O Error: unable to load the graph data ("+error+")"; } catch(error:TypeError) { m_sTitle.text = "Error: unable to load the graph data (Invalid XML data)"; } catch(error:Error) { m_sTitle.text = "Error: unable to load the graph data ("+error+")"; } finally { if (m_oPreloader != null) { removeChild(m_oPreloader); m_oPreloader = null; } } } function onXMLLoadError(event:IOErrorEvent):void { if (m_oPreloader != null) { removeChild(m_oPreloader); m_oPreloader = null; } m_sTitle.text = "I/O Error: unable to load the graph data ("+event+")"; } function parseXMLData(oParentNode:GraphNode, oXMLData:XML, iChildIndex:Number, iChildCount:Number) { //trace(oXMLData.child("node").length()); var oNode:GraphNode; oNode = addNode(oParentNode, oXMLData.child("node")[0], iChildIndex, iChildCount); if (oParentNode != null) { AddLink(oParentNode.GetKey(), oNode.GetKey()); } //trace('Root node:'+oRoot.toString()); var oLinks = oXMLData.child("node")[0].links; var iChildIndex:Number = 0; if (oLinks.length() > 0) { //trace('links: '+oLinks.toString()); var oLink = oLinks.link; for each(var oChild:XML in oLink) { parseXMLData(oNode, oChild, iChildIndex, oLinks.link.length()); iChildIndex++; } } } function addNode(oParent:GraphNode, oXMLData:XML, iChildIndex:Number, iChildCount:Number) { var sClass:String = oXMLData.@obj_class; var sClassName:String = oXMLData.@obj_class_name; var iId = oXMLData.@id; var sLabel:String = oXMLData.@name; var sIcon:String = oXMLData.@icon; var oDetails:Object = new Object; var sZlist:String = oXMLData.@zlist; var oNode:GraphNode = GetNode(sClass+'/'+iId); if (oNode == null) { // If the node does not already exist, let's create it var oPt:Point = GetNextFreePosition(oParent, iChildIndex, iChildCount); var sParentKey = null; if (oParent != null) { sParentKey = oParent.GetKey(); } // Read the details var aDetails:Array; aDetails = sZlist.split(','); for(var i:String in aDetails) { //if (oXMLData.hasOwnProperty('att_'+i)) //{ oDetails[aDetails[i]] = oXMLData.attribute('att_'+i).toString(); //} } oNode = new GraphNode(this, oPt, sClass, sClassName, iId, sLabel, sIcon, sParentKey, m_fZoom, oDetails); this.m_aNodes[oNode.GetKey()] = oNode; //Keep it referenced if (oParent == null) { m_oRootNode = oNode; } m_oCanvas.addChild(oNode); } return oNode; //trace("class: "+sClass+", id: "+iId+", name: "+sLabel+", Icon: "+sIcon); } function GetNode(sKey) { if (m_aNodes.hasOwnProperty(sKey)) { return m_aNodes[sKey]; } else { return null; } } function GetNextFreePosition(oParent:GraphNode, iChildIndex:Number, iChildCount:Number):Point { var oPt:Point = GetInitialPosition(); var angle:Number = GetInitialAngle(); if (oParent != null) { oPt.x = oParent.x; oPt.y = oParent.y; var sGrandParentKey:String = oParent.GetParentKey(); if (sGrandParentKey != null) { var oGrandParent:GraphNode = GetNode(sGrandParentKey); var dx:Number = oParent.x - oGrandParent.x; var dy:Number = oParent.y - oGrandParent.y; if ((dx == 0) && (dy == 0)) { angle = GetInitialAngle(); } else { angle = Math.atan2(dy, dx); } } var nbItemsOnRow:Number = 0; var nbRows:Number = 0; // Determines the position of this element // The elements are placed on circles of maximum m_MAX_ITEMS_PER_ROW elements per row // The last row containing potentially less items // nbRows indicates on which row (first row = 0) the item is to be placed if (iChildCount > m_MAX_ITEMS_PER_ROW) { nbRows = Math.floor(iChildIndex / m_MAX_ITEMS_PER_ROW); if ( iChildIndex > (Math.floor(iChildCount / m_MAX_ITEMS_PER_ROW)*m_MAX_ITEMS_PER_ROW)) { // node is on the last (incomplete) row nbItemsOnRow = (iChildCount % m_MAX_ITEMS_PER_ROW); } else { nbItemsOnRow = m_MAX_ITEMS_PER_ROW; } } else { if (iChildCount == 2) { nbItemsOnRow = 4; // Nicer display than everything aligned at 180 deg. } else { nbItemsOnRow = iChildCount; } } var radius = this.m_RADIUS * (1 + nbRows); angle += (1 - 2*((1+iChildIndex) % 2))*(Math.floor((1+iChildIndex) / 2))*(2*Math.PI) / nbItemsOnRow; oPt.x += radius * Math.cos(angle); oPt.y += radius * 0.7 * Math.sin(angle); // Ellipse because the labels are written horizontally ! //trace("iChildIndex: "+iChildIndex+" (iChildCount: "+iChildCount+") x: "+oPt.x+" y: "+oPt.y+" sGdParentKey: "+sGrandParentKey); } return oPt; } function GetInitialPosition():Point { trace('width: '+stage.stageWidth+' height: '+stage.stageHeight); var oPos:Point = new Point(0,0); switch(m_sStartPosition) { case 'left': oPos.x = m_RADIUS; oPos.y = stage.stageHeight / 2; break; case 'right': oPos.x = stage.stageWidth - m_RADIUS; oPos.y = stage.stageHeight / 2; break; case 'top': oPos.x = stage.stageWidth/2; oPos.y = m_RADIUS; break; case 'bottom': oPos.x = stage.stageWidth/2; oPos.y = stage.stageHeight - m_RADIUS; break; } return oPos; } function GetInitialAngle():Number { var angle:Number; switch(m_sStartPosition) { case 'left': angle = 0; break; case 'right': angle = Math.PI; break; case 'top': angle = -Math.PI / 2; break; case 'right': angle = Math.PI / 2; break; } return angle; } function AddLink(sStart:String, sEnd:String) { var oLink = new Link(sStart, sEnd); m_aLinks.push(oLink); } function drawLines(event:Event):void { var color:uint = 0x666666; m_oCanvas.graphics.clear(); m_oCanvas.graphics.lineStyle(2,0x666666,100); UpdatePositions(); for (var index:String in m_aLinks) { var oStartNode:GraphNode = GetNode(m_aLinks[index].GetStart()); var oEndNode = GetNode(m_aLinks[index].GetEnd()); m_oCanvas.graphics.moveTo(oStartNode.x, oStartNode.y); m_oCanvas.graphics.lineTo(oEndNode.x, oEndNode.y); var oMiddlePoint:Point = new Point((oEndNode.x+oStartNode.x)/2, (oEndNode.y+oStartNode.y)/2); drawArrow(oMiddlePoint, oEndNode.x - oStartNode.x, oEndNode.y - oStartNode.y, color); } if (this.m_FOCUS_DELAY_COUNTDOWN > 0) { this.m_FOCUS_DELAY_COUNTDOWN--; trace('FOCUS_DELAY:'+this.m_FOCUS_DELAY_COUNTDOWN); UpdatePanAndZoom(m_FOCUS_DELAY_COUNTDOWN / 30); } else if (this.m_FOCUS_DELAY_COUNTDOWN == 0) { // Increase the friction so that manually manipulating objects gets easier trace("More friction now..."); m_Ks = 5*m_Ks; // 5 times more friction this.m_FOCUS_DELAY_COUNTDOWN--; } } function drawArrow(oPt:Point, dx:Number, dy:Number, color:uint):void { var l:Number = Math.sqrt(dx*dx+dy*dy); var arrowSize:Number = 5; if (l > 0) { m_oCanvas.graphics.lineStyle(2,color,100,false,"normal",CapsStyle.ROUND); m_oCanvas.graphics.moveTo(oPt.x, oPt.y); m_oCanvas.graphics.lineTo(oPt.x + arrowSize*(dy-dx)/l, oPt.y - arrowSize*(dx+dy)/l); m_oCanvas.graphics.moveTo(oPt.x, oPt.y); m_oCanvas.graphics.lineTo(oPt.x - arrowSize*(dx+dy)/l, oPt.y - arrowSize*(dy-dx)/l); } } public function ReadParam(sName:String, sDefaultValue:String) { var paramObj:Object = LoaderInfo(this.root.loaderInfo).parameters; if (paramObj.hasOwnProperty(sName)) { return unescape(paramObj[sName]); } else { return sDefaultValue; } } public function ComputeElectrostaticForces():Array { var aForces:Array = new Array; //trace('====== BEGIN ComputeElectrostaticForces() ======='); for (var i:String in this.m_aNodes) { aForces[i] = new Object; aForces[i].FxTotal = 0; aForces[i].FyTotal = 0; var oCurrentNode:GraphNode = GetNode(i); for (var j:String in this.m_aNodes) { if (i != j) { var oRemoteNode:GraphNode = GetNode(j); var dx:Number = oRemoteNode.x - oCurrentNode.x; var dy:Number = oRemoteNode.y - oCurrentNode.y; var d2:Number = (dx*dx + dy*dy) / (this.m_RADIUS * this.m_RADIUS); var d:Number = Math.sqrt(d2); var Fx:Number = 0; var Fy:Number = 0; if (d2 < 0.05) { d2 = 0.05; } if (d2 < 2 ) // Full influence under 2 * m_RADIUS px { Fx = -this.m_Q * dx / d2; Fy = -this.m_Q * dy / d2; aForces[i].FxTotal += Fx; aForces[i].FyTotal += Fy; } else if (d2 < 4 ) // Decrease the influence to between 4 and 2 * m_RADIUS px { Fx = -this.m_Q * (4 - d2) * dx / d2; Fy = -this.m_Q * (4 - d2) * dy / d2; aForces[i].FxTotal += Fx; aForces[i].FyTotal += Fy; } } } } //for (i in this.m_aNodes) //{ // trace('ELECTROSTATIC forces on '+i+': Fx='+aForces[i].FxTotal+', Fy='+aForces[i].FyTotal); // if (Math.abs(aForces[i].FyTotal) > 1) // { // for (i in this.m_aNodes) // { // var oNode:GraphNode = GetNode(i); // trace('node: '+i+' (x='+oNode.x+', y='+oNode.y+')'); // } // } //} //trace('====== END ComputeElectrostaticForces() ======='); return aForces; } function ComputeElasticForces() { //trace('====== BEGIN ComputeElasticForces() ======='); var aForces:Array = new Array; for (var i:String in this.m_aNodes) { aForces[i] = new Object; aForces[i].FxTotal = 0; aForces[i].FyTotal = 0; } // Elastic forces: each link applies a force proportional to its length (F = - K * x) for(i in this.m_aLinks) { var oStartNode:GraphNode = GetNode(m_aLinks[i].GetStart()); var oEndNode = GetNode(m_aLinks[i].GetEnd()); var dx = oStartNode.x - oEndNode.x; var dy = oStartNode.y - oEndNode.y; //d = Math.sqrt(dx*dx + dy*dy); //Fx = -K * d * dx / d; //Fy = -K * d * dy / d; // Links with more weight attached are more rigid ! //weightCoeff = (aWeights[aLinks[l].start] + aWeights[aLinks[l].end])/2; var Fx = -this.m_K * dx; var Fy = -this.m_K * dy; aForces[oStartNode.GetKey()].FxTotal += Fx; aForces[oStartNode.GetKey()].FyTotal += Fy; aForces[oEndNode.GetKey()].FxTotal -= Fx; aForces[oEndNode.GetKey()].FyTotal -= Fy; } //for (i in this.m_aNodes) //{ // trace('Elastic forces on '+i+': Fx='+aForces[i].FxTotal+', Fy='+aForces[i].FyTotal); // if (Math.abs(aForces[i].FyTotal) > 1) // { // for (i in this.m_aNodes) // { // var oNode:GraphNode = GetNode(i); // trace('node: '+i+' (x='+oNode.x+', y='+oNode.y+')'); // } // } //} //trace('====== END ComputeElasticForces() ======='); return aForces; } /** * Update the nodes' position based on their current movement and the forces applied */ function UpdatePositions() { //trace('====== BEGIN UpdatePositions() ======='); var aElasticForces:Array = ComputeElasticForces(); var aElectrostaticForces:Array = ComputeElectrostaticForces(); //DrawForces(aElectrostaticForces, 0xcc0000); //DrawForces(aElectrostaticForces, 0x0000cc); for (var i:String in this.m_aNodes) { var oNode:GraphNode = GetNode(i); if (!oNode.m_bInDrag) { var Fx:Number = aElasticForces[i].FxTotal + aElectrostaticForces[i].FxTotal; var Fy:Number = aElasticForces[i].FyTotal + aElectrostaticForces[i].FyTotal; if ( (Fx*Fx + Fy*Fy) < (this.m_Ks*this.m_Ks)) { // Movement is less than minimum level (solid friction) => object is stopped // otherwise let's keep it moving oNode.m_speed_x = 0; oNode.m_speed_y = 0; //trace('object '+i+' stopped ! (x='+oNode.x+', y='+oNode.y+')'); } else { oNode.m_speed_x = oNode.m_speed_x*this.m_Kf + this.m_deltaT*Fx; oNode.m_speed_y = oNode.m_speed_y*this.m_Kf + this.m_deltaT*Fy; var dx:Number = oNode.m_speed_x * this.m_deltaT; var dy:Number = oNode.m_speed_y * this.m_deltaT; oNode.x = Math.round(oNode.x + dx); oNode.y = Math.round(oNode.y + dy); //trace('object '+i+' moves (Force: Fx='+Fx+', Fy='+Fy+')! '); } } } //trace('======= Updated Positions ========='); //DumpPositions(); //trace('====== END UpdatePositions() ======='); } public function DrawForces(aForces:Array, color:uint) { for (var i:String in aForces) { var oNode:GraphNode = GetNode(i); var oForce:Object = aForces[i]; m_oCanvas.graphics.lineStyle(2,color,100,false,"normal",CapsStyle.ROUND); m_oCanvas.graphics.moveTo(oNode.x, oNode.y); var oEndPoint:Point = new Point; oEndPoint.x = Math.round(oNode.x + oForce.FxTotal); oEndPoint.y = Math.round(oNode.y + oForce.FyTotal); m_oCanvas.graphics.lineTo(oEndPoint.x, oEndPoint.y); drawArrow(oEndPoint, oForce.FxTotal, oForce.FyTotal, color); //trace('Drawinf vector '+i+': (x='+oNode.x+', y='+oNode.y+') to (x='+oEndPoint.x+', y='+oEndPoint.y+')'); } } public function UpdatePanAndZoom(countDownRatio:Number) { var sceneRect:Rectangle = null; for(var i:String in this.m_aNodes) { if (sceneRect == null) { sceneRect = GetNode(i).getBounds(m_oCanvas); } else { sceneRect = sceneRect.union(GetNode(i).getBounds(m_oCanvas)); } } if (sceneRect != null) { var idealZoomLevel:Number = 1; trace('Stage dimensions: width:'+stage.stageWidth+' height:'+stage.stageHeight); if ((sceneRect.width > stage.stageWidth) || (sceneRect.height > (stage.stageHeight - 50))) { var wRatio:Number = stage.stageWidth / sceneRect.width; var hRatio:Number = (stage.stageHeight - 50) / sceneRect.height; idealZoomLevel = Math.min(wRatio, hRatio); SetZoomLevel(idealZoomLevel); m_oZoomSlider.value = Math.round(100*idealZoomLevel); } var xOffset:Number = 0; var yOffset:Number = 50; if (stage.stageWidth > sceneRect.width) { xOffset = (stage.stageWidth-sceneRect.width)/2 } if (stage.stageHeight > sceneRect.height) { yOffset = 50 + (stage.stageHeight-50-sceneRect.height)/2 } m_oCanvas.x = xOffset-sceneRect.x; m_oCanvas.y = yOffset-sceneRect.y; trace('Scene bounding rect: x:'+sceneRect.x+' y:'+sceneRect.y+' width:'+sceneRect.width+' height:'+sceneRect.height+' zoomLevel:'+idealZoomLevel); } } public function DumpPositions() { for (var i:String in this.m_aNodes) { var oNode:GraphNode = GetNode(i); trace(i+' Position: (x='+oNode.x+', y='+oNode.y+')'); } } } } class Link extends Object { protected var m_sStart:String; protected var m_sEnd:String; public function Link(sStartNodeKey:String, sEndNodeKey:String) { m_sStart = sStartNodeKey; m_sEnd = sEndNodeKey; } public function GetStart():String { return m_sStart; } public function GetEnd():String { return m_sEnd; } } import flash.display.*; import flash.geom.*; import flash.events.*; class NavigatorCanvas extends Sprite { public function NavigatorCanvas() { } }