Browse Source

- Updated flash navigator: better objects placement, auto-zoom & pan at startup, improved tooltip with some details on each objects.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@710 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 14 years ago
parent
commit
9445d94183

+ 32 - 4
navigator/iTop/GraphNode.as

@@ -5,6 +5,7 @@
 	import flash.net.*;
 	import flash.events.*;
 	import flash.text.*; 
+	import flash.xml.*; 
 	import flash.ui.ContextMenu;
 	import flash.ui.ContextMenuItem;
 	import iTop.ToolTip;
@@ -14,27 +15,46 @@
 	public class GraphNode extends Sprite
 	{
 		private var m_oIcon:Loader;
-		private var m_sClass;String;
+		private var m_sClass:String;
+		private var m_sClassName:String;
 		private var m_iId:Number;
 		private var m_sParentKey:String;
 		private var m_oToolTip:ToolTip;
 		private var m_fZoom:Number;
 		private var m_oParent:Navigator;
+		private static const ROUND:Number = 20;
+		private static const PADDING:Number = 5;
+		public var m_speed_x:Number = 0;
+		public var m_speed_y:Number = 0;
+		public var m_bInDrag:Boolean = false;
 		
-		public function GraphNode(oParent:Navigator, oPt:Point, sClass:String, iId:Number, sLabel:String, sIconPath:String, sParentKey:String, fZoom:Number)
+		public function GraphNode(oParent:Navigator, oPt:Point, sClass:String, sClassName:String, iId:Number, sLabel:String, sIconPath:String, sParentKey:String, fZoom:Number, oDetails:Object)
 		{
 			x = oPt.x;
 			y = oPt.y;
 			m_fZoom = fZoom;
 			m_sClass = sClass;
+			m_sClassName = sClassName;
 			m_iId = iId;
+			m_sLabel.autoSize = TextFieldAutoSize.LEFT;
+			m_sLabel.multiline = false;
 			m_sLabel.text = sLabel;
-			m_sLabel.autoSize = TextFieldAutoSize.CENTER;
 			m_sLabel.width = m_sLabel.textWidth;
 			m_sLabel.x = -m_sLabel.width/2;
 			m_sLabel.height = m_sLabel.textHeight;
+			// Draw the background
+			graphics.beginFill( 0xf1f1f6, 0.8 );
+			graphics.drawRoundRect( m_sLabel.x -PADDING, m_sLabel.y - PADDING, m_sLabel.width+PADDING*2, m_sLabel.height+PADDING*2, ROUND );
+			graphics.endFill();
+			
 			m_sParentKey = sParentKey;
-			m_oToolTip = new ToolTip( "<p><b>"+m_sClass+"</b></p><p>"+sLabel+"</p>");
+			var sTooltipText:String = "<p><b>"+m_sClassName+"</b></p><p>"+sLabel+"</p>";
+			for (var s:String in oDetails)
+			{
+				sTooltipText += '<p>'+s+': '+oDetails[s]+'</p>';
+			}
+			trace('Tooltip text: '+sTooltipText);
+			m_oToolTip = new ToolTip(sTooltipText);
 			m_oToolTip.scaleX = 1 / m_fZoom;
 			m_oToolTip.scaleY = 1 / m_fZoom;
 			m_oParent = oParent;
@@ -94,11 +114,14 @@
 		{
 			trace("Click in Node");
 			m_oParent.m_bChildDragging = true;
+			m_bInDrag = true;
+			m_oToolTip.timer.stop(); // Don't show the tooltip while dragging
 			startDrag(); 
 		} 
 		
 		function mouseReleased(event:MouseEvent):void 
 		{ 
+			m_bInDrag = false;
 			stopDrag(); 
 			m_oParent.m_bChildDragging = false;
 		}
@@ -130,5 +153,10 @@
 				return sDefaultValue;
 			}
 		}
+		        
+		public function GetLabelWidth():Number
+		{
+			return m_sLabel.width;
+		}
 	}
 }

+ 329 - 34
navigator/iTop/Navigator.as

@@ -1,4 +1,4 @@
-package iTop
+package iTop
 {
 	import flash.display.*;
 	import flash.geom.*;
@@ -8,7 +8,7 @@
 	import fl.controls.Slider; 
 	import fl.events.SliderEvent; 
 	import fl.controls.Label; 
- 	
+
 	// The main canvas
 	public class Navigator extends MovieClip
 	{
@@ -28,8 +28,14 @@
 		protected var m_sObjId:String;
 		
 		// Constants
-		protected var m_RADIUS = 120;
-		protected var m_ITEMS_PER_ROW = 8;
+		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 = 50; // 50 images to zoom & pan correctly
 		protected var m_fZoom:Number;
 		
 		// Constructor
@@ -41,6 +47,8 @@
 			initParameters();
 			doLoadData();
 			addEventListener(Event.ENTER_FRAME, initGraphics);
+			//Stop scaling the flash content
+			stage.scaleMode = StageScaleMode.NO_SCALE;
 		}
 		
 		protected function initParameters():void
@@ -62,11 +70,6 @@
 			m_oCanvas.scaleY = m_fZoom;
 			// Handle listeners...
 			removeEventListener(Event.ENTER_FRAME,initGraphics);
-			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); 
 		}
 		function mouseDown(event:MouseEvent):void 
 		{ 
@@ -87,14 +90,24 @@
 		
 		function onZoomChange(event:SliderEvent):void
 		{ 
-    		m_fZoom = event.value/100;
+    		SetZoomLevel(event.value/100);
+		}
+		
+		function SetZoomLevel(fZoomLevel:Number):void
+		{
+			m_fZoom = fZoomLevel;
 			m_oCanvas.scaleX = m_fZoom;
 			m_oCanvas.scaleY = m_fZoom;
 		}
 		
 		function doLoadData()
 		{
-			var myString:String = m_sDataUrl+'?relation='+m_sRelation+'&class='+m_sObjClass+'&id='+m_sObjId;
+			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();
@@ -109,10 +122,17 @@
 			var myXML:XML = XML(m_oLoader.data);
 			//trace("Data loaded." + myXML);
 			//trace("===========================");
-			parseXMLData(null, myXML, 0);
+			parseXMLData(null, myXML, 0, 0);
 			m_sTitle.text = myXML.attribute("title");
 			m_oZoomSlider.enabled = true;
 			removeChild(m_oPreloader);
+			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();
 		}
 
 		function onXMLLoadError(event:IOErrorEvent):void
@@ -120,11 +140,11 @@
 			trace("An error occured:" + Event);
 		}
 		
-		function parseXMLData(oParentNode:GraphNode, oXMLData:XML, iChildIndex:Number)
+		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);
+			oNode  = addNode(oParentNode, oXMLData.child("node")[0], iChildIndex, iChildCount);
 			if (oParentNode != null)
 			{
 				AddLink(oParentNode.GetKey(), oNode.GetKey());
@@ -138,30 +158,43 @@
 				var oLink = oLinks.link;
 				for each(var oChild:XML in oLink)
 				{
-					parseXMLData(oNode, oChild, iChildIndex);
+					parseXMLData(oNode, oChild, iChildIndex, oLinks.link.length());
 					iChildIndex++;
 				}
 			}
 		}
 		
-		function addNode(oParent:GraphNode, oXMLData:XML, iChildIndex)
+		function addNode(oParent:GraphNode, oXMLData:XML, iChildIndex:Number, iChildCount:Number)
 		{
-			var sClass  = oXMLData.@obj_class;
+			var sClass:String  = oXMLData.@obj_class;
+			var sClassName:String  = oXMLData.@obj_class_name;
 			var iId = oXMLData.@id;
-			var sLabel = oXMLData.@name;
-			var sIcon = oXMLData.@icon;
+			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);
+				var oPt:Point = GetNextFreePosition(oParent, iChildIndex, iChildCount);
 				var sParentKey = null;
 				if (oParent != null)
 				{
 					sParentKey = oParent.GetKey();
 				}
-				oNode = new GraphNode(this, oPt, sClass, iId, sLabel, sIcon, sParentKey, m_fZoom);
+				// 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)
 				{
@@ -185,7 +218,7 @@
 			}
 		}
 		
-		function GetNextFreePosition(oParent:GraphNode, iChildIndex:Number):Point
+		function GetNextFreePosition(oParent:GraphNode, iChildIndex:Number, iChildCount:Number):Point
 		{
 			var oPt:Point = GetInitialPosition();
 			var angle:Number = GetInitialAngle();
@@ -208,13 +241,43 @@
 						angle = Math.atan2(dy, dx);
 					}
 				}
-				var radius = m_RADIUS * Math.floor(iChildIndex / m_ITEMS_PER_ROW);
-				angle += iChildIndex*(2*Math.PI) / m_ITEMS_PER_ROW;
+				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 += m_RADIUS * Math.cos(angle);
-				oPt.y += m_RADIUS * Math.sin(angle);
+				oPt.x += radius * Math.cos(angle);
+				oPt.y += radius * 0.7 * Math.sin(angle); // Ellipse because the labels are written horizontally !
 				
-				trace("iChildIndex: "+iChildIndex+" x: "+oPt.x+" y: "+oPt.y+" sGdParentKey: "+sGrandParentKey);
+				//trace("iChildIndex: "+iChildIndex+" (iChildCount: "+iChildCount+") x: "+oPt.x+" y: "+oPt.y+" sGdParentKey: "+sGrandParentKey);
 			}
 			return oPt;
 		}
@@ -280,8 +343,10 @@
 		
 		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());
@@ -289,18 +354,28 @@
 				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);
+				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);
 			}
 		}
-		function drawArrow(oPt:Point, dx:Number, dy:Number):void
+		function drawArrow(oPt:Point, dx:Number, dy:Number, color:uint):void
 		{
 			var l:Number = Math.sqrt(dx*dx+dy*dy);
 			var arrowSize:Number = 5;
-			m_oCanvas.graphics.lineStyle(2,0x666666,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);
+			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)
@@ -316,6 +391,226 @@
 				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+')');
+			}
+		}
 	}	
 }
 

+ 68 - 20
navigator/iTop/ToolTip.as

@@ -1,6 +1,7 @@
-package iTop
+package iTop
 {
-	import flash.display.Sprite;
+	import flash.display.*;
+	import flash.geom.*;
 	import flash.text.TextField;
 	import flash.text.TextFormat;
 	import flash.text.TextFieldAutoSize;
@@ -15,17 +16,18 @@
 		// You'll need this for proper text formatting
 		private var _tf:TextField = new TextField();
 		private var _format:TextFormat = new TextFormat();
-		private static const ROUND:Number = 2;
+		private static const ROUND:Number = 10;
 		private static const HEIGHT:Number = 25;
 		private static const FONT_SIZE:uint = 12;
 		private static const FONT:String = 'Arial';
 		private static const PADDING:Number = 5;
-		private static const MIN_ALPHA:Number = 0;
-		private static const ALPHA_INC:Number = .1;
+		private static const MIN_ALPHA:Number = 0.0;
+		private static const ALPHA_INC:Number = 0.1;
 		private static const MAX_ALPHA:Number = 1;
-		private static const REFRESH:Number = MAX_ALPHA / ALPHA_INC;
-		// For fading in and out
-		private var _timer:Timer;
+		private static const REFRESH:Number = (MAX_ALPHA - MIN_ALPHA) / ALPHA_INC;
+		private static const APPEAR_TIMEOUT = 1000; // ms
+		// For appearence, fading in and out
+		public var timer:Timer;
 		public function ToolTip( tip:String )
 		{
 			// Hold onto the tip for posterity
@@ -51,7 +53,7 @@
 			_tf.y += PADDING;
 			addChild( _tf );
 			// Draw the background
-			graphics.beginFill( 0xF6F6F1, 1 );
+			graphics.beginFill( 0xEEEE99, 0.95 );
 			graphics.drawRoundRect( 0, 0, _tf.textWidth+PADDING*4, _tf.textHeight+PADDING*4, ROUND );
 			graphics.endFill();
 			this.alpha = MIN_ALPHA;
@@ -65,28 +67,74 @@
 		}
 		public function mouse_over( e:MouseEvent ):void
 		{
-			this.parent.setChildIndex( this, this.parent.numChildren-1 )
-			// Move the tool tip to the top!
-			var fadeSpeed:Number = 500 / REFRESH;
-			_timer = new Timer( fadeSpeed, REFRESH );
-			_timer.addEventListener( "timer", fadeIn );
-			_timer.start();
+			// Make the tooltip appear smoothly after a delay
+			if (this.timer != null)
+			{
+				this.timer.stop();
+			}
+			this.timer = new Timer( APPEAR_TIMEOUT, 1 );
+			this.timer.addEventListener( "timer", appear );
+			this.timer.start();
 			this.parent.addEventListener( MouseEvent.MOUSE_OUT, mouse_out );
 		}
 		public function mouse_out( e:MouseEvent ):void
 		{
 			var fadeSpeed:Number = 500 / REFRESH;
-			_timer = new Timer( fadeSpeed, REFRESH );
-			_timer.addEventListener( "timer", fadeOut );
-			_timer.start();
+			if (this.timer != null)
+			{
+				this.timer.stop();
+			}
+			this.timer = new Timer( fadeSpeed, REFRESH );
+			this.timer.addEventListener( "timer", fadeOut );
+			this.timer.start();
+			this.parent.removeEventListener( MouseEvent.MOUSE_OUT, mouse_out );
+		}
+		
+		private function appear(i:uint):void
+		{
+			// The delay has elapsed, show (smoothly) the tooltip
+			if (this.timer != null)
+			{
+				this.timer.stop();
+			}
+			// Reuse the time for the fadeIn
+			this.parent.setChildIndex( this, this.parent.numChildren-1 )
+			// Move the tool tip to the top!
+			var fadeSpeed:Number = 500 / REFRESH;
+			this.alpha = MIN_ALPHA;
+			if (this.timer != null)
+			{
+				this.timer.stop();
+			}
+			this.timer = new Timer( fadeSpeed, REFRESH );
+			this.timer.addEventListener( "timer", fadeIn );
+			this.timer.start();
+			this.parent.addEventListener( MouseEvent.MOUSE_OUT, mouse_out );
 		}
+		
 		private function fadeIn( i:uint ):void
 		{
-			this.alpha += ALPHA_INC;
+			if (this.alpha < (1.0 - ALPHA_INC))
+			{
+				this.alpha += ALPHA_INC;
+			}
+			else
+			{
+				this.alpha = 1.0;
+			}
+			//trace("++ Tooltip alpha: "+this.alpha+" ALPHA_INC:"+ALPHA_INC);
 		}
 		private function fadeOut( i:uint ):void
 		{
-			this.alpha -= ALPHA_INC;
+			if (this.alpha > ALPHA_INC)
+			{
+				this.alpha -= ALPHA_INC;
+			}
+			else
+			{
+				this.alpha = 0.0;
+			}
+			//trace("-- Tooltip alpha: "+this.alpha+" ALPHA_INC:"+ALPHA_INC);
 		}
 	}
 }

BIN
navigator/navigator.fla


BIN
navigator/navigator.swf


+ 6 - 5
pages/xml.navigator.php

@@ -37,7 +37,7 @@ define('MAX_RECURSION_DEPTH', 20);
  */ 
 function AddNodeDetails(&$oNode, $oObj)
 {
-	$aZlist = MetaModel::GetZListItems(get_class($oObj), 'details');
+	$aZlist = MetaModel::GetZListItems(get_class($oObj), 'list');
 	$aLabels = array();
 	$index = 0;
 	foreach($aZlist as $sAttCode)
@@ -46,7 +46,7 @@ function AddNodeDetails(&$oNode, $oObj)
 		$aLabels[] = $oAttDef->GetLabel();
 		if (!$oAttDef->IsLinkSet())
 		{
-			$oNode->SetAttribute('att_'.$index, $oObj->Get($sAttCode));
+			$oNode->SetAttribute('att_'.$index, $oObj->GetAsHTML($sAttCode));
 		}
 		$index++;
 	}
@@ -78,6 +78,7 @@ function GetRelatedObjects(DBObject $oObj, $sRelationName, &$oLinks, &$oXmlDoc,
 				$oLinkedNode = $oXmlDoc->CreateElement('node');
 				$oLinkedNode->SetAttribute('id', $oTargetObj->GetKey());
 				$oLinkedNode->SetAttribute('obj_class', get_class($oTargetObj));
+				$oLinkedNode->SetAttribute('obj_class_name', MetaModel::GetName(get_class($oTargetObj)));
 				$oLinkedNode->SetAttribute('name', $oTargetObj->GetName());
 				$oLinkedNode->SetAttribute('icon', BuildIconPath($oTargetObj->GetIcon(false /* No IMG tag */)));
 				AddNodeDetails($oLinkedNode, $oTargetObj);
@@ -104,11 +105,10 @@ function BuildIconPath($sIconPath)
 
 require_once('../application/startup.inc.php');
 require_once('../application/loginwebpage.class.inc.php');
-//// For developping the Navigator
+// For developping the Navigator from within Flash
 //session_start();
 //$_SESSION['auth_user'] = 'admin';
-//$_SESSION['auth_pwd'] = 'admin2';
-//UserRights::Login($_SESSION['auth_user'], $_SESSION['auth_pwd']); // Set the user's language
+//UserRights::Login($_SESSION['auth_user']); // Set the user's language
 LoginWebPage::DoLogin(); // Check user rights and prompt if needed
 
 $oPage = new ajax_page("");
@@ -134,6 +134,7 @@ if ($id != 0)
 	$oXmlNode = $oXmlDoc->CreateElement('node');
 	$oXmlNode->SetAttribute('id', $oObj->GetKey());
 	$oXmlNode->SetAttribute('obj_class', get_class($oObj));
+	$oXmlNode->SetAttribute('obj_class_name', MetaModel::GetName(get_class($oObj)));
 	$oXmlNode->SetAttribute('name', $oObj->GetName());
 	$oXmlNode->SetAttribute('icon', BuildIconPath($oObj->GetIcon(false /* No IMG tag */))); // Hard coded for the moment
 	AddNodeDetails($oXmlNode, $oObj);