
package jp.riken.brain.ni.samuraigraph.figure.java2d;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;

import jp.riken.brain.ni.samuraigraph.base.SGAxis;
import jp.riken.brain.ni.samuraigraph.base.SGDefaultValues;
import jp.riken.brain.ni.samuraigraph.base.SGIAxisBreakElement;
import jp.riken.brain.ni.samuraigraph.base.SGIAxisElement;
import jp.riken.brain.ni.samuraigraph.base.SGIConstants;
import jp.riken.brain.ni.samuraigraph.base.SGICopiable;
import jp.riken.brain.ni.samuraigraph.base.SGIFigureElement;
import jp.riken.brain.ni.samuraigraph.base.SGIGraphElement;
import jp.riken.brain.ni.samuraigraph.base.SGIGridElement;
import jp.riken.brain.ni.samuraigraph.base.SGILegendElement;
import jp.riken.brain.ni.samuraigraph.base.SGIMovable;
import jp.riken.brain.ni.samuraigraph.base.SGINode;
import jp.riken.brain.ni.samuraigraph.base.SGISelectable;
import jp.riken.brain.ni.samuraigraph.base.SGIShapeElement;
import jp.riken.brain.ni.samuraigraph.base.SGISignificantDifferenceElement;
import jp.riken.brain.ni.samuraigraph.base.SGIStringElement;
import jp.riken.brain.ni.samuraigraph.base.SGITimingLineElement;
import jp.riken.brain.ni.samuraigraph.base.SGIUndoable;
import jp.riken.brain.ni.samuraigraph.base.SGProperties;
import jp.riken.brain.ni.samuraigraph.base.SGPropertyDialog;
import jp.riken.brain.ni.samuraigraph.base.SGTuple2f;
import jp.riken.brain.ni.samuraigraph.base.SGUndoManager;
import jp.riken.brain.ni.samuraigraph.base.SGUtility;
import jp.riken.brain.ni.samuraigraph.base.SGUtilityNumber;
import jp.riken.brain.ni.samuraigraph.base.SGUtilityText;
import jp.riken.brain.ni.samuraigraph.figure.SGDrawingElementArrow;
import jp.riken.brain.ni.samuraigraph.figure.SGDrawingElementLine;
import jp.riken.brain.ni.samuraigraph.figure.SGFigureElement;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;



/**
 *
 *
 */
public class SGTimingLineElement extends SGFigureElement
	implements SGITimingLineElement
{

	/**
	 * 
	 */	
	private SGIAxisElement mAxisElement = null;


	/**
	 * 
	 */
	private ArrayList mTimingLineList = new ArrayList();


	/**
	 * 
	 */
	private SGTimingLineDialog mDialog = null;



	/**
	 * 
	 *
	 */
	public SGTimingLineElement()
	{
		super();
		this.init();
	}


	/**
	 * 
	 *
	 */
	private void init()
	{
		SGDrawingElementArrow2D a = new SGDrawingElementArrow2D();

		a.setStartHeadType( SGDrawingElementArrow.SYMBOL_TYPE_VOID );
		a.setEndHeadType( SGDrawingElementArrow.SYMBOL_TYPE_ARROW_HEAD );
		a.setLineWidth( 3.0f );
		a.setLineType( SGDrawingElementLine.LINE_TYPE_SOLID );
		a.setColor( Color.RED );
//		a.setHeadLineWidth( 2.0f );
		a.setHeadSize( 12.0f );
		a.setHeadOpenAngle( (float)Math.PI/5.0f );
		a.setHeadCloseAngle( (float)Math.PI/3.0f );

		a.setVisible(false);

		this.mGuideArrow = a;
	}



	/**
	 * 
	 */
	public String toString()
	{
		return "SGTimingLineEement";
	}


	/**
	 * 
	 * @return
	 */
	public String getClassDescription()
	{
		return "Timing Lines";
	}


	/**
	 * 
	 * @param element
	 */
	public void setAxisElement( final SGIAxisElement element )
	{
		this.mAxisElement = element;
	}



	/**
	 * 
	 * @param x
	 * @param y
	 */
	public void guideToAdd( final int x, final int y )
	{

		if( this.getGraphRect().contains(x,y) == false )
		{
			this.mGuideArrow.setVisible(false);
			this.repaint();
			return;
		}

		ArrayList configList = new ArrayList();
		ArrayList distList = new ArrayList();
		this.getNearestAxisInfo( x, y, configList, distList );
		final int config = ((Integer)configList.get(0)).intValue();
		final float distance = ((Number)distList.get(0)).floatValue();

		final float length = distance;

		SGTuple2f start = new SGTuple2f(x,y);
		SGTuple2f end = new SGTuple2f();
		if( config == SGIAxisElement.AXIS_HORIZONTAL_1 )
		{
			end.x = x;
			end.y = y + length;
		}
		else if( config == SGIAxisElement.AXIS_HORIZONTAL_2 )
		{
			end.x = x;
			end.y = y - length;
		}
		else if( config == SGIAxisElement.AXIS_PERPENDICULAR_1 )
		{
			end.x = x - length;
			end.y = y;
		}
		else if( config == SGIAxisElement.AXIS_PERPENDICULAR_2 )
		{
			end.x = x + length;
			end.y = y;
		}

		this.mGuideArrow.setTermPoints( start, end );

		this.mGuideArrow.setVisible(true);
		this.repaint();
	}



	private void getNearestAxisInfo(
		final int x, final int y, final ArrayList configList, final ArrayList distList )
	{
		
		final float left = this.getGraphRectX();
		final float right = left + this.getGraphRectWidth();
		final float top = this.getGraphRectY();
		final float bottom = top + this.getGraphRectHeight();

		final float dl = x - left;
		final float dr = right - x;
		final float dt = y - top;
		final float db = bottom - y;

		float[] array = {dl,dr,dt,db};
		float min = Float.MAX_VALUE;
		for( int ii=0; ii<array.length; ii++ )
		{
			if( array[ii] < min )
			{
				min = array[ii];
			}
		}

		int config = -1;
		if( min==db )
		{
			config = SGIAxisElement.AXIS_HORIZONTAL_1;
		}
		else if( min==dt )
		{
			config = SGIAxisElement.AXIS_HORIZONTAL_2;
		}
		else if( min==dl )
		{
			config = SGIAxisElement.AXIS_PERPENDICULAR_1;
		}
		else if( min==dr )
		{
			config = SGIAxisElement.AXIS_PERPENDICULAR_2;
		}

		configList.add( new Integer(config) );
		distList.add( new Float(min) );
	}



	/**
	 * 
	 */
	public boolean addTimingLine( final int x, final int y )
	{
		if( this.getGraphRect().contains(x,y) == false )
		{
			return false;
		}

		final ArrayList configList = new ArrayList();
		final ArrayList distList = new ArrayList();
		this.getNearestAxisInfo( x, y, configList, distList );
		final int config = ((Integer)configList.get(0)).intValue();

		final SGAxis axis = this.mAxisElement.getAxisInPlane( config );
		final double value = this.mAxisElement.getValue( config, x, y );

		// create an instance
		final TimingLine line = new TimingLine();

		line.setAxis( axis );
		line.setValue( value );
		line.setMagnification( this.getMagnification() );

		// add to the list
		this.addToList( line );

		// hide the guide
		this.mGuideArrow.setVisible(false);

		// set focus
		this.updateFocusedObjectsList( line, 0 );

		// update the history
		this.setChanged(true);
		this.notifyToRoot();

		return true;
	}



	
	private int mIDCounter = 0;

	private void addToList( final TimingLine line )
	{
		// location
		line.initDrawingElement(2);
		line.setDrawingElementsLocation();

		// properties
		line.setPropertiesOfDrawingElements();
		line.initPropertiesHistory();

		// add to the list
		this.mTimingLineList.add( line );

		// set ID
		line.mID = this.mIDCounter;
		this.mIDCounter++;
	}
	
	
	

	/**
	 * 
	 * @return
	 */
	public boolean setDialogOwner( final Frame frame )
	{
		super.setDialogOwner(frame);
		this.createTimingLineDialog();
		return true;
	}


	/**
	 * 同期を取る
	 */
	public boolean synchronize( final SGIFigureElement element )
	{

		boolean flag = true;
		if( element instanceof SGILegendElement )
		{

		}
		else if( element instanceof SGIAxisElement )
		{
			final SGIAxisElement aElement = (SGIAxisElement)element;
			flag = this.synchronizeToAxisElement( aElement );
		}
		else if( element instanceof SGIStringElement )
		{
			
		}
		else if( element instanceof SGIGraphElement )
		{
			
		}
		else if( element instanceof SGIAxisBreakElement )
		{
			
		}
		else if( element instanceof SGISignificantDifferenceElement )
		{
			
		}
		else if( element instanceof SGITimingLineElement )
		{
			
		}
		else if( element instanceof SGIGridElement )
		{

		}
		else if( element instanceof SGIShapeElement )
		{

		}
		else
		{
			flag = this.synchronizeArgument( element );
		}

		return flag;
	}


	/**
	 * Synchronize the element given by the argument.
	 * @param element An object to be synchronized.
	 */
	public boolean synchronizeArgument( final SGIFigureElement element )
	{
	    // this shouldn't happen
	    throw new Error();
	}


	/**
	 * 
	 */
	public boolean updateHistory()
	{
		return this.updateHistory( this.getVisibleTimingElementList() );
	}


	/**
	 * 
	 */
	public SGProperties getProperties()
	{
		SGProperties p = new TimingElementProperties();
		if( this.getProperties(p) == false )
		{
			return null;
		}
		return p;
	}



	/**
	 * 
	 */
	public String getTagName()
	{
		return TAG_NAME_TIMING_LINES;
	}
	
	

	/**
	 * 
	 */
	public boolean writeProperty( final Element el )
	{
		return true;
	}


	
	/**
	 * 
	 */
	public boolean readProperty( final Element element )
	{
		NodeList nList = element.getElementsByTagName( TimingLine.TAG_NAME_TIMING_LINE );
		for( int ii=0; ii<nList.getLength(); ii++ )
		{
			Node node = nList.item(ii);
			if( node instanceof Element )
			{
				Element el = (Element)node;
				TimingLine line = new TimingLine();
				if( line.readProperty(el) == false )
				{
					return false;
				}
				this.addToList( line );
			}
		}
		
		return true;
	}

	
	/**
	 * 
	 */
	public Element createElement( Document document )
	{
		Element el = this.createThisElement( document );
		if( el==null )
		{
			return null;
		}

		ArrayList list = this.getVisibleTimingElementList();
		for( int ii=0; ii<list.size(); ii++ )
		{
			TimingLine line = (TimingLine)list.get(ii);
			if( line.isValid() == false )
			{
				continue;
			}
			Element elTiming = line.createElement( document );
			if( elTiming==null )
			{
				return null;
			}
			el.appendChild( elTiming );
		}
		return el;
	}



	/**
	 * 
	 */
	public boolean isChanged()
	{
		if( super.isChanged() )
		{
			return true;
		}
		ArrayList list = this.getVisibleTimingElementList();
		for( int ii=0; ii<list.size(); ii++ )
		{
			SGIUndoable el = (SGIUndoable)list.get(ii);
			if( el.isChanged() )
			{
				return true;
			}
		}
		return false;
	}



	/**
	 * ズーム
	 */
	public boolean zoom( final float ratio )
	{
		super.zoom(ratio);

		ArrayList list = this.mTimingLineList;
		for( int ii=0; ii<list.size(); ii++ )
		{
			final SGTimingLine line = (SGTimingLine)list.get(ii);
			line.zoom(ratio);
		}

		return true;
	}


	/**
	 * 
	 */
	public boolean setGraphRect(
		final float x, final float y, final float width, final float height )
	{
		super.setGraphRect(x,y,width,height);
		if( this.setAllDrawingElementsLocation() == false )
		{
			return false;
		}
		return true;
	}


	/**
	 * タイミング線の位置を決定
	 */

	private boolean calcLocationOfTimingLine(
		final double value,
		final SGAxis axis,
		final boolean horizontal,
		final SGTuple2f startPoint,
		final SGTuple2f endPoint )
	{

		final float location = this.calcLocation( value, axis, horizontal );
		if( Float.isNaN( location ) )
		{
			return false;
		}

		Rectangle2D rect = this.getGraphRect();
		if( horizontal )
		{
			startPoint.x = location;
			startPoint.y = (float)rect.getY();
			endPoint.x = location;
			endPoint.y = startPoint.y + (float)( rect.getHeight() );
		}
		else
		{
			startPoint.x = (float)rect.getX();
			startPoint.y = location;
			endPoint.x = startPoint.x + (float)( rect.getWidth() );
			endPoint.y = location;
		}

		return true;
	}



	/**
	 * 
	 */
	private boolean createTimingLineDialog()
	{
		final SGTimingLineDialog dg = new SGTimingLineDialog( mDialogOwner, true );

		this.mDialog = dg;

		return true;
	}



	/**
	 * 
	 * @return
	 */
	private ArrayList getVisibleTimingElementListInside()
	{
		ArrayList list = new ArrayList();
		ArrayList lList = this.getVisibleTimingElementList();
		for( int ii=0; ii<lList.size(); ii++ )
		{
			SGTimingLine line = (SGTimingLine)lList.get(ii);
			if( this.isInsideRange( line ) )
			{
				list.add( line );
			}
		}

		return list;
	}



	/**
	 * 
	 */
	private ArrayList getVisibleTimingElementList()
	{
		ArrayList list = new ArrayList();
		ArrayList lList = this.mTimingLineList;
		for( int ii=0; ii<lList.size(); ii++ )
		{
			SGTimingLine line = (SGTimingLine)lList.get(ii);
			if( line.isVisible() )
			{
				list.add( line );
			}
		}

		return list;
	}



	/**
	 * Returns a list of child nodes.
	 * @return a list of chid nodes
	 */
	public ArrayList getChildNodes()
	{
		return this.getVisibleTimingElementList();
	}


	/**
	 * 
	 */
	public boolean getFocusedObjectsList( ArrayList list )
	{
		ArrayList elList = this.getVisibleTimingElementListInside();
		for( int ii=0; ii<elList.size(); ii++ )
		{
			TimingLine line = (TimingLine)elList.get(ii);
			if( line.isSelected() )
			{
				list.add(line);
			}
		}
		return true;
	}


	/**
	 * 
	 *
	 */
	public boolean hideSelectedObject( SGISelectable s )
	{
		TimingLine el = (TimingLine)s;
		el.setVisible(false);
		return true;
	}

	
	/**
	 * 
	 * @param groupSet
	 * @return
	 */
	private boolean isInsideRange( SGTimingLine groupSet )
	{
		SGAxis axis = groupSet.mAxis;
		double value = groupSet.mValue;
		return ( axis.insideRange(value) );
	}
	
	
	/**
	 * 
	 */
	protected boolean setVisibleTimingElements( final ArrayList list )
	{
		ArrayList visibleList = new ArrayList( list );
		ArrayList invisibleList = new ArrayList();
		ArrayList lineList = this.mTimingLineList;

		for( int ii=0; ii<lineList.size(); ii++ )
		{
			SGTimingLine line = (SGTimingLine)lineList.get(ii);
			final boolean b = list.contains(line);
			line.setVisible(b);
			if(!b)
			{
				invisibleList.add( line );
			}
		}

		lineList.clear();
		for( int ii=0; ii<visibleList.size(); ii++ )
		{
			lineList.add( visibleList.get(ii) );
		}
		for( int ii=0; ii<invisibleList.size(); ii++ )
		{
			lineList.add( invisibleList.get(ii) );
		}

		return true;
	}





	/**
	 * 
	 */
	public boolean onMouseClicked( final MouseEvent e )
	{

		ArrayList list = this.getVisibleTimingElementListInside();
		for( int ii=list.size()-1; ii>=0; ii-- )
		{
			final TimingLine line = (TimingLine)list.get(ii);
			if( line.isValid() == false )
			{
				continue;
			}
			if( this.clickDrawingElements(line,e) )
			{
				return true;
			}
		}

		return false;
	}

	
	/**
	 * 
	 */
	private boolean clickDrawingElements(
		final TimingLine el, final MouseEvent e )
	{
		final int x = e.getX();
		final int y = e.getY();
		final int cnt = e.getClickCount();

		// clicked on the line elements
		if( el.contains(x,y) )
		{
			if( cnt==1 )
			{
				this.updateFocusedObjectsList( el, e );

				if( SwingUtilities.isLeftMouseButton(e) )
				{

				}
				else if( SwingUtilities.isRightMouseButton(e) )
				{
					el.getPopupMenu().show( this.getComponent(), x, y );
				}
			}
			else if( cnt==2 )
			{
				if( SwingUtilities.isLeftMouseButton(e) )
				{
					this.setPropertiesOfSelectedObjects();
				}
			}

			return true;
		}

		return false;
	}




	/**
	 * 
	 */
	public boolean onMousePressed( final MouseEvent e )
	{

		final int x = e.getX();
		final int y = e.getY();

		ArrayList list = this.getVisibleTimingElementListInside();
		for( int ii=list.size()-1; ii>=0; ii-- )
		{
			final TimingLine el = (TimingLine)list.get(ii);
			if( el.isValid() == false )
			{
				continue;
			}
			if( el.contains(x,y) )
			{
				return true;
			}
		}

		return false;
	}


	/**
	 * 
	 */
	public boolean onMouseDragged( final MouseEvent e )
	{
		return true;
	}


	/**
	 * 
	 */
	public boolean onMouseReleased( final MouseEvent e )
	{
		return true;
	}
	
	
	/**
	 * 
	 */
	public boolean onDrawingElement( final int x, final int y )
	{
		ArrayList list = this.getVisibleTimingElementListInside();
		for( int ii=list.size()-1; ii>=0; ii-- )
		{
			final TimingLine el = (TimingLine)list.get(ii);
			if( el.isValid() == false )
			{
				continue;
			}
			final boolean flag = el.contains(x,y);
			if( flag )
			{
				this.setMouseCursor( Cursor.HAND_CURSOR );
				return true;
			}
		}

		return false;
	}



	/**
	 * 
	 * @return
	 */
	public ArrayList getPropertyDialogObserverList()
	{
		return this.getFocusedObjectsList();
	}


	
	/**
	 * 
	 * @return
	 */
	public boolean setTemporaryPropertiesOfFocusedObjects()
	{
		ArrayList list = this.getFocusedObjectsList();
		for( int ii=0; ii<list.size(); ii++ )
		{
			TimingLine el = (TimingLine)list.get(ii);
			el.mTemporaryProperties = el.getProperties();
		}
		return true;
	}



	/**
	 * 
	 * @return
	 */
	public boolean setChangedFocusedObjects()
	{
		ArrayList list = this.getFocusedObjectsList();
		for( int ii=0; ii<list.size(); ii++ )
		{
			TimingLine el = (TimingLine)list.get(ii);
			SGProperties temp = el.mTemporaryProperties;
			if( temp!=null )
			{
				SGProperties p = el.getProperties();
				if( p.equals(temp)==false )
				{
					el.setChanged(true);
				}
			}
		}
		return true;
	}



	/**
	 * An arrow for the guide to add a timing line.
	 */
	private SGDrawingElementArrow2D mGuideArrow = null;


	
	/**
	 * 
	 */
	public void paintGraphics( Graphics g, boolean clip )
	{
		Graphics2D g2d = (Graphics2D)g;
		
		ArrayList list = this.getVisibleTimingElementListInside();
		Rectangle2D gRect = this.getGraphRect();


		// draw guide arrow
		if( this.mGuideArrow.isVisible() )
		{
			this.mGuideArrow.paint( g2d, gRect );
		}


		// グラフ描画領域のクリッピング
		if( clip )
		{
			SGUtilityForFigureElement.clipGraphRect(this,g2d);
		}


		// draw timing line
		for( int ii=0; ii<list.size(); ii++ )
		{
			TimingLine tl = (TimingLine)list.get(ii);
			if( tl.isValid() == false )
			{
				continue;
			}
			if( clip )
			{
				tl.paintElement(g2d);
			}
			else
			{
				tl.paintElement(g2d,gRect);
			}
		}


		if( clip )
		{
			g2d.setClip( this.getViewBounds() );
		}


		// draw symbols around all objects
		if( this.mSymbolsVisibleFlagAroundAllObjects )
		{
			for( int ii=0; ii<list.size(); ii++ )
			{
				TimingLine el = (TimingLine)list.get(ii);
				if( el.isValid() == false )
				{
					continue;
				}
				ArrayList pList = el.getAnchorPointList();
				SGUtilityForFigureElement.drawAnchorAsChildObject(
					pList, g2d );
			}
		}

		// draw symbols around focused objects
		if( this.mSymbolsVisibleFlagAroundFocusedObjects )
		{
			ArrayList fList = new ArrayList();
			this.getFocusedObjectsList( fList );
			for( int ii=0; ii<fList.size(); ii++ )
			{
				TimingLine el = (TimingLine)fList.get(ii);
				if( el.isValid() == false )
				{
					continue;
				}
				ArrayList pList = el.getAnchorPointList();
				SGUtilityForFigureElement.drawAnchorAsFocusedObject(
					pList, g2d );
			}
		}

	}



	/**
	 * Move the focused objects to front or back.
	 * @param toFront - flag whether to front or back
	 * @return true:succeeded, false:failed
	 */
	public boolean moveFocusedObjects( boolean toFront )
	{
		return this.moveFocusedObjects( toFront, this.mTimingLineList );
	}



	/**
	 * 
	 * @param line
	 * @return
	 */
	protected boolean hideTimingElementGroupSet( final SGTimingLine line )
	{
		line.setVisible(false);
		notifyChange();
		this.setChanged(true);
		this.notifyToRoot();
		return true;
	}



	/**
	 * 
	 */
	protected boolean removeTimingElementGroupSet( final SGTimingLine line )
	{
		this.mTimingLineList.remove(line);
		return false;
	}



	/**
	 * 
	 */
	protected boolean synchronizeToAxisElement( final SGIAxisElement aElement )
	{
		return this.setAllDrawingElementsLocation();
	}


	/**
	 * 全描画要素の位置を決定
	 */
	protected boolean setAllDrawingElementsLocation()
	{
		ArrayList list = this.getVisibleTimingElementList();
		for( int ii=0; ii<list.size(); ii++ )
		{
			final TimingLine line
				= (TimingLine)list.get(ii);
			if( line.setDrawingElementsLocation() == false )
			{
				return false;
			}
		}

		return true;
	}



	/**
	 * 
	 */
	public boolean getProperties( final SGProperties p )
	{
		if( ( p instanceof TimingElementProperties ) == false )
		{
			return false;
		}

		TimingElementProperties gp = (TimingElementProperties)p;
		gp.visibleTimingElementList = this.getVisibleTimingElementList();

		return true;
	}


	/**
	 * 
	 */
	public boolean setProperties( final SGProperties p )
	{
		if( ( p instanceof TimingElementProperties ) == false )
		{
			return false;
		}

		TimingElementProperties gp = (TimingElementProperties)p;


		boolean flag;
		flag = this.setVisibleTimingElements( gp.visibleTimingElementList );
		if( !flag )
		{
			return false;
		}

		return true;

	}



	/**
	 * Create copies of the focused objects.
	 * @return
	 */
	public boolean duplicateFocusedObjects()
	{
		final int ox = (int)( this.mMagnification*OFFSET_DUPLICATED_OBJECT_X );
		final int oy = (int)( this.mMagnification*OFFSET_DUPLICATED_OBJECT_Y );

		ArrayList cList = this.duplicateObjects();
		for( int ii=0; ii<cList.size(); ii++ )
		{
			// duplicate
			TimingLine el = (TimingLine)cList.get(ii);

			// translate the duplicate
			el.translate( ox, oy );

//			el.setDrawingElementsLocation();

			// set selected
			el.setSelected(true);
			
			// add to the list
			this.addToList( el );
		}

		if( cList.size()!=0 )
		{
			this.setChanged(true);
		}
		
//		// set the location of drawing elements
//		this.setAllDrawingElementsLocation();

//		this.repaint();

		return true;
	}



	/**
	 * Paste the objects.
	 * @param list of the objects to be pasted
	 * @return true:succeeded, false:failed
	 */
	public boolean paste( ArrayList list )
	{
		final float mag = this.getMagnification();
		final int ox = (int)( mag*OFFSET_DUPLICATED_OBJECT_X );
		final int oy = (int)( mag*OFFSET_DUPLICATED_OBJECT_Y );

		int cnt = 0;
		for( int ii=0; ii<list.size(); ii++ )
		{
			Object obj = list.get(ii);
			if( obj instanceof TimingLine )
			{
				TimingLine line = (TimingLine)obj;

				// translate the instance to be pasted
				line.translate( ox, oy );

				SGProperties p = line.getProperties();
				
				TimingLine el = new TimingLine();
				el.setMagnification(mag);
				el.setProperties(p);

				el.mAxis = this.mAxisElement.getAxisInCube( line.mTempAxis );

//				el.setDrawingElementsLocation();

				// add to the list
				this.addToList(el);

				// initialize history
				el.initPropertiesHistory();

				cnt++;
			}
		}

//		this.setAllDrawingElementsLocation();
		
		if( cnt!=0 )
		{
			this.setChanged(true);
		}

//		this.repaint();

		return true;
	}



	/**
	 * 
	 *
	 */
	class TimingLine extends SGTimingLine
		implements ActionListener, SGIUndoable, SGISelectable, SGICopiable,
			SGIMovable, SGITimingLineDialogObserver, SGINode
	{
		
		/**
		 * 
		 */
		private int mID;
		
		
		/**
		 * Flag whether this object is focused.
		 */
		private boolean mSelectedFlag = false;


		/**
		 * Get the flag as a focused object.
		 * @return whether this object is focused.
		 */
		public boolean isSelected()
		{
			return this.mSelectedFlag;
		}


		/**
		 * Set the flag as a focused object.
		 * @param b focused
		 */
		public void setSelected( final boolean b )
		{
			this.mSelectedFlag = b;
		}


		/**
		 * 
		 */		
		public Color getColor()
		{
			return this.getColor(0);
		}

		
		/**
		 * 
		 */
		private SGProperties mTemporaryProperties = null;


		/**
		 * 
		 */
		public boolean prepare()
		{
			this.mTemporaryProperties = this.getProperties();
			return true;
		}


		/**
		 * 
		 */
		private JPopupMenu mPopupMenu = new JPopupMenu();

		
		/**
		 * 
		 *
		 */
		protected TimingLine()
		{
			super();
			this.init();
		}
		
		
		/**
		 * 
		 */
		private boolean init()
		{
			this.setLineWidth( SGDefaultValues.TIMING_LINE_WIDTH );
			Integer n = SGDrawingElementLine.getLineTypeFromName( SGDefaultValues.TIMING_LINE_TYPE );
			if( n==null )
			{
				return false;
			}
			this.setLineType( n.intValue() );
			this.setColor( SGDefaultValues.TIMING_LINE_COLOR );

			this.createPopupMenu();

			return true;
		}

		
		/**
		 * 
		 * @return
		 */
		public String getClassDescription()
		{
			return "";
		}


		/**
		 * 
		 * @return
		 */
		public String getInstanceDescription()
		{
			String str = "";
			str += this.mID + ": ";

			String config = SGTimingLineElement.this.mAxisElement.getAxisLocation( this.mAxis );
			str += config + ", ";

			String s;
			if( this.isHorizontal() )
			{
				s = "X";
			}
			else
			{
				s = "Y";
			}
			str += s + "=" + this.getValue();

			return str;
		}


		/**
		 * 
		 * @return
		 */
		private ArrayList getAnchorPointList()
		{
			ArrayList list = new ArrayList();

			Point2D pos0 = new Point2D.Float();
			Point2D pos1 = new Point2D.Float();
			
			final boolean flag = this.isHorizontal();
			float location = calcLocation( this.mValue, this.mAxis, flag );
			Rectangle2D rect = getGraphRect();
			if( flag )
			{
				final float y0 = (float)rect.getY();
				final float y1 = y0 + (float)rect.getHeight();
				pos0.setLocation( location, y0 );
				pos1.setLocation( location, y1 );
			}
			else
			{
				final float x0 = (float)rect.getX();
				final float x1 = x0 + (float)rect.getWidth();
				pos0.setLocation( x0, location );
				pos1.setLocation( x1, location );
			}
			
			list.add( pos0 );
			list.add( pos1 );

			return list;
		}

		
		
		/**
		 * 
		 */
		private boolean createPopupMenu()
		{
			JPopupMenu p = this.mPopupMenu;
			
			p.setBounds( 0, 0, 100, 100 );

			p.add( new JLabel( "  -- Timing Line --" ) );
			p.addSeparator();

			SGUtility.addItem( p, this, MENUCMD_MOVE_TO_FRONT );
			SGUtility.addItem( p, this, MENUCMD_MOVE_TO_BACK );

			p.addSeparator();

			SGUtility.addItem( p, this, MENUCMD_CUT );
			SGUtility.addItem( p, this, MENUCMD_COPY );
			SGUtility.addItem( p, this, MENUCMD_PASTE );

			p.addSeparator();

			SGUtility.addItem( p, this, MENUCMD_DELETE );
			SGUtility.addItem( p, this, MENUCMD_DUPLICATE );

			p.addSeparator();

			SGUtility.addItem( p, this, MENUCMD_PROPERTY );

			return true;
		}


		/**
		 * 
		 */
		public JPopupMenu getPopupMenu()
		{
			return this.mPopupMenu;
		}


		/**
		 * 
		 */
		public int getAxisConfiguration()
		{
			return SGTimingLineElement.this.mAxisElement.getConfigurationInPlane( this.mAxis );
		}


		/**
		 * 
		 */
		public void setAxisConfiguration( final int config )
		{
			SGIAxisElement aElement = SGTimingLineElement.this.mAxisElement;
			SGAxis axis = aElement.getAxisInPlane( config );
			this.mAxis = axis;
		}



		/**
		 * Called when the location of data points have changed.
		 */
		private boolean setDrawingElementsLocation()
		{
			final SGAxis axis = this.mAxis;
			final double value = this.mValue;
			final boolean horizontal = this.isHorizontal();

			SGTuple2f[] array = new SGTuple2f[2];
			array[0] = new SGTuple2f();
			array[1] = new SGTuple2f();
			
			if( calcLocationOfTimingLine(
					value,
					axis,
					horizontal,
					array[0],
					array[1] ) == false )
			{
				this.setValid(false);
				return false;
			}
			else
			{
				this.setValid(true);
			}

			if( this.setLocation( array ) == false )
			{
				return false;
			}

			return true;
		}



		private boolean mValidFlag = true;

		public boolean isValid()
		{
			return this.mValidFlag;
		}

		public void setValid( final boolean b )
		{
			this.mValidFlag = b;
		}


		/**
		 * Translate the object.
		 * @param dx
		 * @param dy
		 */
		public void translate( float dx, float dy )
		{
			SGAxis axis = this.mAxis;
			final boolean horizontal = this.isHorizontal();
			float location = SGTimingLineElement.this.calcLocation( this.getValue(), axis, horizontal );
			float locationNew;
			if( this.isHorizontal() )
			{
				locationNew = location + dx;
			}
			else
			{
				locationNew = location + dy;
			}
			double value = SGTimingLineElement.this.calcValue( locationNew, axis, horizontal );
			value = SGTimingLineElement.this.getNumberInRangeOrder( value, axis );
			this.setValue( value );
		}
		
		
		/**
		 * 
		 * @return
		 */
		private boolean isHorizontal()
		{
			final ArrayList hAxisList = mAxisElement.getHorizontalAxisList();
			final ArrayList pAxisList = mAxisElement.getPerpendicularAxisList();

			Boolean hFlag = null;
			if( hAxisList.contains( this.mAxis ) )
			{
				hFlag = Boolean.TRUE;
			}
			if( pAxisList.contains( this.mAxis ) )
			{
				hFlag = Boolean.FALSE;
			}
			if( hFlag == null )
			{
				throw new Error("");
			}

			return hFlag.booleanValue();
		}
		
		
		
		/**
		 * 
		 * @param config
		 * @param value
		 * @return
		 */
		public boolean hasValidValue( final int config, final Number value )
		{
			final SGAxis axis = (config==-1) ? this.mAxis : mAxisElement.getAxisInPlane( config );
			final double v = (value!=null) ? value.doubleValue() : this.getValue();
			return axis.isValidValue(v);
		}



		/**
		 * 
		 */
		public void actionPerformed( final ActionEvent e )
		{
			final Object source = e.getSource();
			final String command = e.getActionCommand();

			if( command.equals( MENUCMD_PROPERTY ) )
			{
				SGTimingLineElement.this.setPropertiesOfSelectedObjects();
			}
			else if(
				command.equals( MENUCMD_COPY )
				| command.equals( MENUCMD_CUT )
				| command.equals( MENUCMD_PASTE )
				| command.equals( MENUCMD_DELETE )
				| command.equals( MENUCMD_DUPLICATE )
				| command.equals( MENUCMD_MOVE_TO_FRONT )
				| command.equals( MENUCMD_MOVE_TO_BACK )
			)
			{
				notifyToListener( command );
			}

		}



		/**
		 * 
		 */
		public boolean commit()
		{
			SGProperties pTemp = this.mTemporaryProperties;
			SGProperties pPresent = this.getProperties();
			if( pTemp.equals(pPresent) == false )
			{
				this.setChanged(true);
			}
			this.mTemporaryProperties = null;

			this.setDrawingElementsLocation();

			repaint();
			notifyChange();

			return true;
		}


		/**
		 * 
		 */
		public boolean cancel()
		{
			if( this.setProperties( this.mTemporaryProperties ) == false )
			{
				return false;
			}

			this.setDrawingElementsLocation();

			repaint();
			notifyChange();

			return true;
		}



		/**
		 * 
		 */
		public boolean preview()
		{
			this.setDrawingElementsLocation();

			repaint();
			notifyChange();

			return true;
		}


		/**
		 * Returns a property dialog.
		 * @return property dialog
		 */
		public SGPropertyDialog getPropertyDialog()
		{
			return SGTimingLineElement.this.mDialog;
		}


		/**
		 * Returns a list of child nodes.
		 * @return a list of chid nodes
		 */
		public ArrayList getChildNodes()
		{
			return new ArrayList();
		}



		//
		private SGUndoManager mUndoManager = new SGUndoManager(this);


		/**
		 * 
		 * @return
		 */
		public SGProperties getMemento()
		{
			return this.getProperties();
		}


		/**
		 * 
		 * @param p
		 * @return
		 */
		public boolean setMemento( SGProperties p )
		{
			return this.setProperties(p);
		}


		/**
		 * 
		 * @return
		 */
		public boolean isUndoable()
		{
			return this.mUndoManager.isUndoable();
		}

	
		/**
		 * 
		 * @return
		 */
		public boolean isRedoable()
		{
			return this.mUndoManager.isRedoable();
		}


		/**
		 * 
		 */
		public boolean initPropertiesHistory()
		{
			return this.mUndoManager.initPropertiesHistory();
		}


		/**
		 * アンドゥを行う
		 */
		public boolean setMementoBackward()
		{
			if( this.mUndoManager.setMementoBackward() == false )
			{
				return false;
			}

			setAllDrawingElementsLocation();
			notifyChange();

			return true;
		}


		/**
		 * リドゥを行う
		 */
		public boolean setMementoForward()
		{
			if( this.mUndoManager.setMementoForward() == false )
			{
				return false;
			}

			setAllDrawingElementsLocation();
			notifyChange();

			return true;
		}



		/**
		 * アンドゥ操作を依頼する
		 */
		public boolean undo()
		{
			return this.setMementoBackward();
		}



		/**
		 * リドゥ操作を依頼する
		 */
		public boolean redo()
		{
			return this.setMementoForward();
		}


		/**
		 * 履歴の更新の実行。アンドゥ対象の操作が行われたときに、真っ先に呼ばれる。
		 */
		public boolean updateHistory()
		{
			return this.mUndoManager.updateHistory();
		}


		/**
		 * 
		 *
		 */
		public void notifyToRoot()
		{
			SGTimingLineElement.this.notifyToRoot();
		}


		/**
		 * 
		 * @return
		 */
		public boolean isChanged()
		{
			return this.mChangedFlag;
		}

		
		private boolean mChangedFlag = false;

		
		public void setChanged( final boolean b )
		{
			this.mChangedFlag = b;
		}

		
	
		
		/**
		 * 
		 */
		public Element createElement( final Document document )
		{
			Element element = document.createElement( this.getTagName() );
			if( this.writeProperty( element ) == false )
			{
				return null;
			}
			return element;
		}


		
		public static final String TAG_NAME_TIMING_LINE = "TimingLine";
		
		public String getTagName()
		{
			return TAG_NAME_TIMING_LINE;
		}
		
		

		/**
		 * 
		 */
		public boolean writeProperty( final Element el )
		{
			el.setAttribute( KEY_AXIS_POSITION, mAxisElement.getAxisLocation( this.mAxis ) );
			el.setAttribute( KEY_VALUE, Double.toString( this.mValue ) );
			el.setAttribute( KEY_LINE_WIDTH, Float.toString( this.mLineWidth ) + SGUtilityNumber.pt );
			el.setAttribute( KEY_LINE_TYPE, SGDrawingElementLine.getLineTypeName( this.mLineType ) );
			el.setAttribute( KEY_COLOR_LIST, SGUtilityText.getColorListString( this.mColorList ) );
			return true;
		}

		
		/**
		 * 
		 * @param el
		 * @param p
		 * @return
		 */
		public boolean readProperties( final Element el, SGProperties p )
		{
			if( ( p instanceof TimingLineProperties ) == false ) return false;
			
			TimingLineProperties tp = (TimingLineProperties)p;
			
			
			final String cm = SGUtilityNumber.cm;
			final String pt = SGUtilityNumber.pt;
			final float ratio = SGIConstants.CM_POINT_RATIO;

			String str = null;
			Number num = null;
			Color cl = null;
			Boolean b = null;
			ArrayList list = null;


			// line width
			str = el.getAttribute( KEY_LINE_WIDTH );
			if( str.length()==0 )
			{
				return false;
			}
			num = SGUtilityText.getDouble(str,pt);
			if( num==null )
			{
				return false;
			}
			tp.setLineWidth( num.floatValue() );


			// line type
			str = el.getAttribute( KEY_LINE_TYPE );
			if( str.length()==0 )
			{
				return false;
			}
			num = SGDrawingElementLine.getLineTypeFromName(str);
			if( num==null )
			{
				return false;
			}
			tp.setLineType( num.intValue() );


			// color list
			str = el.getAttribute( KEY_COLOR_LIST );
			if( str.length()==0 )
			{
				return false;
			}
			list = SGUtilityText.getColorList(str);
			if( list==null )
			{
				return false;
			}
			tp.setColorList( list );

			
			// value
			str = el.getAttribute( KEY_VALUE );
			if( str.length()==0 )
			{
				return false;
			}
			num = SGUtilityText.getDouble(str);
			if( num==null )
			{
				return false;
			}
			tp.setValue( num.doubleValue() );

			
			str = el.getAttribute( KEY_AXIS_POSITION );
			if( str.length()==0 )
			{
				return false;
			}
			final int config = mAxisElement.getConfigurationInCube(str);
			SGAxis axis = mAxisElement.getAxisInCube( config );
			if( axis==null )
			{
				return false;
			}
			tp.setAxis( axis );

			
			// not from the property file
			tp.setVisible(true);
			
			
			return true;
		}

	
	
	
		/**
		 * 
		 */
		public Object copy()
		{
			TimingLine el = new TimingLine();
			el.setMagnification( this.mMagnification );
			el.setProperties( this.getProperties() );
			el.mTempAxis = mAxisElement.getConfigurationInCube( this.mAxis );
			return el;
		}

		private int mTempAxis = -1;

	}

	
	
	
	/**
	 * 
	 */
	public static class TimingElementProperties extends SGProperties
	{

		ArrayList visibleTimingElementList = new ArrayList();


		/**
		 * 
		 *
		 */
		public TimingElementProperties()
		{
			super();
		}


		/**
		 * 
		 */
		public boolean equals( final Object obj )
		{
			if( ( obj instanceof TimingElementProperties ) == false )
			{
				return false;
			}

			TimingElementProperties p = (TimingElementProperties)obj;
			if( p.visibleTimingElementList.equals(this.visibleTimingElementList) == false )
			{
				return false;
			}

			return true;
		}


		/**
		 * 
		 */
		public String toString()
		{
			String str = new String("[");
			str += this.visibleTimingElementList.toString();
			str += new String("]");

			return str;
		}
		
	}



}
