PipeProfileCAD.sml

  Download

More scripts: Display Toolbar

Syntax Highlighing:

comments, key words, predefined symbols, class members & methods, functions & classes
            
# PipeProfileCAD.sml
#
# This script is meant to be run as a Toolscript
#
# Current Assumptions:
# In the group there are two layers:
# Layer 1: DEM - not needed
# Layer 2: Overlaying vector lines (must be last layer)
#
# Purpose:
# The script creates a window displaying a plotted profile of the
# selected lines nearest line, using the vector DB values for elevation.
# Only allows selection of contiguous connected lines, but can add to
# either end of selected line set.  Active elements are highlighted in
# both the View and Line Profile window on mouseover in either window.
#
# Usage:
# Left click: toggle select/deselect line
# Right click: Accept current line and draw
# Save As button on Line Profile window saves the current graph to a CAD object.
#
# Requires TNTmips 7.0 dated 1 March 2005 or later.
## MicroImages, Inc.
## Revised 19 April 2005. Fixed inconsistency in placement of graph axis labels in
## Line Profile window and CAD. 
# GUI global definitions
class GUI_DLG dlgwin;
class GUI_CANVAS canvas;
class GC gc;			# global graphics context for drawing canvas in dialog
class GRDEVICE_CAD deviceCAD;
class GUI_CTRL_TOGGLEBUTTON gridToggle;
class GUI_CTRL_EDIT_NUMBER gridIntervalX;
class GUI_CTRL_EDIT_NUMBER gridIntervalY;
class GUI_CTRL_TOGGLEBUTTON demToggle;
class GUI_CTRL_EDIT_NUMBER graphMinSetting;
class GUI_CTRL_EDIT_NUMBER graphMaxSetting;
class GUI_CTRL_TOGGLEBUTTON fillToggle;
#class GUI_CTRL_EDIT_NUMBER elemDisplay;
#class GUI_CTRL_EDIT_NUMBER lineDisplay;
#class GUI_CTRL_EDIT_NUMBER vertexDisplay;
class GUI_CTRL_EDIT_NUMBER mouseXDisplay;
class GUI_CTRL_EDIT_NUMBER mouseYDisplay;
class GUI_CTRL_EDIT_NUMBER slopeDisplay;
class GUI_CTRL_EDIT_NUMBER diameterDisplay;
class GUI_CTRL_EDIT_STRING materialDisplay;
# Group/layer/object global definitions
class GRE_GROUP activegroup;
class TRANSPARM mapTrans;
class GRE_LAYER_VECTOR vectorLayer;
class GRE_LAYER_RASTER rasterLayer;
class VECTOR lineVector;
class GEOREF vecGeoref;
class RASTER dem;
string vectorName$;
class DBTABLEINFO nodeTable;
class DBTABLEINFO lineTable;
numeric dbIsInit=0;
class CAD CADgraph;
# Graphical display global definitions
class POLYLINE pipeBottom, pipeTop, pipeFace, surface;
class POLYLINE surface, manholeDepth, demSurface, smoothedSurface;
class STRINGLIST manholeNames;
class POLYLINE pipeBottomSave, pipeTopSave, pipeFaceSave; # used for highlighting
class STRINGLIST colors;
numeric drawable=0, demSet=0;
numeric graphMinZ, graphMaxZ, dataMinZ, dataMaxZ;
numeric setDefaultWhenClose = false;
numeric leftGraphOffset, bottomGraphOffset=20, rightGraphOffset=15, topGraphOffset=50; #5,15
numeric currentlyActive = -1;
numeric needsRedraw = 0;
# Utility global definitions
class POLYLINE demLine;
class STRINGLIST elemList;
class STRINGLIST orderedElemList;
class STRINGLIST reversedList;
array numeric endNodes[2];
numeric MAX_NUMBER = 9999999;
# <debug>
func writePolyline(class FILE f, class POLYLINE polyline)
{
	local class POINT2D p2d;
	local numeric i;
	for (i=0; i<polyline.GetNumPoints(); i++)
	{
		p2d = polyline.GetVertex(i);
		fwritestring(f, sprintf("(%.3f, %.3f)\n", p2d.x, p2d.y));
	}
	fwritestring(f, "\n");
}
# </debug>
func assignColors()
{
	colors.AddToEnd("red");
	colors.AddToEnd("orange");
	colors.AddToEnd("yellow");
	colors.AddToEnd("green");
	colors.AddToEnd("blue");
	colors.AddToEnd("purple");
	colors.AddToEnd("violet");
}
func class COLOR getColor(numeric index)
{
	local class COLOR c;
	c.Name = colors.GetString(index%colors.GetNumItems()-1);
	return c;
}
# Determine if the dem will be used or not
func doUseDEM()
{
	return demToggle.GetValue();
}
# Get the line table by name - return 1 if successful, 0 if table is null
func initLineTable(string tablename)
{
	local class DATABASE lineDB = OpenVectorLineDatabase(lineVector);
	if (lineDB==0) return 0;
	lineTable = DatabaseGetTableInfo(lineDB, tablename);
	if (lineTable==0) return 0;
	return 1;
}
# Get the node table by name - return 1 if successful, 0 if table is null
func initNodeTable(string tablename)
{
	local class DATABASE nodeDB = OpenVectorNodeDatabase(lineVector);
	if (nodeDB==0) return 0;
	nodeTable = DatabaseGetTableInfo(nodeDB, tablename);
	if (nodeTable==0) return 0;
	return 1;
}
# Checks layer to see if it is valid.
func checkLayer()
{
	if (doUseDEM())
	{
		# Raster is assumed to be first layer
		rasterLayer = activegroup.FirstLayer;
		if (rasterLayer.Type!="Raster")
		{
			demSet = 0;
			PopupMessage("The first layer is not a raster, the DEM was not assigned properly and the script may not function properly.");
		}
		DispGetRasterFromLayer(dem, rasterLayer);
		demSet = 1;
	}
	# Vector is assumed to be last layer
	vectorLayer = activegroup.LastLayer;
	DispGetVectorFromLayer(lineVector, vectorLayer);
	vecGeoref = GetLastUsedGeorefObject(lineVector);
	mapTrans.InputProjection = vectorLayer.Projection;
	mapTrans.OutputProjection = rasterLayer.Projection;
	# initialize the database tables
	initLineTable("SEW_INFO");
	initNodeTable("MH_GIS");
	return 1;
}
# Callback for when the active layer changes.
proc cbLayer()
{
	checkLayer();
}
# Callback for when the active group changes.
proc cbGroup()
{
	activegroup = Layout.ActiveGroup;
	WidgetAddCallback(activegroup.LayerSelectedCallback, cbLayer);
	cbLayer();
}
# return the max of two numbers
func max(numeric n1, numeric n2)
{
	if (n1 > n2) return n1;
	return n2;
}
# Get the element from our list
func getElement(class STRINGLIST list, numeric index)
{
	return StrToNum(list.GetString(index));
}
# Get the index of the element in our list, return index if found, -1 otherwise
func indexOf(class STRINGLIST list, numeric value)
{
	local numeric i;
	for (i=0; i<list.GetNumItems(); i++)
	{
		if (getElement(list, i)==value) 
		{
			return i;
		}
	}
	return -1;
}
# Determine if the two lines are consecutive (endpt1 == startpt2)
#func isConsecutive(class POLYLINE line, numeric vertexNum)
#{
#	if (indexOf(endPoints, vertexNum)>-1)
#	{
#		local class POINT2D point1 = line.GetVertex(vertexNum);
#		local class POINT2D point2 = line.GetVertex(vertexNum+1);
#
#		if (point1!=point2)
#		{
#			return 0;
#		}
#	}
#	return 1;
#}
# Compute the distance between two points
func computeDistance(class POINT2D p1, class POINT2D p2)
{
	return sqrt((p2.x-p1.x)^2 + (p2.y-p1.y)^2);
}
# Determine if the given lin, col is within the raster extents
func isPointInRaster(numeric lin, numeric col)
{
	if (lin<1 || col<1 || lin>NumLins(dem) || col>NumCols(dem)) return 0;
	return 1;
}
# Get the value of the first record attached to the node "nodeNum" as a string
func string readNodeTableRecordStr(numeric nodeNum, string field)
{
	local array numeric records[1];
	TableReadAttachment(nodeTable, nodeNum, records, "node");
	local string retVal = TableReadFieldStr(nodeTable, field, records[1]);
	return retVal;
}
# Get the value of the first record attached to the line "lineNum" as a string
func string readLineTableRecordStr(numeric lineNum, string field)
{
	local array numeric records[1];
	TableReadAttachment(lineTable, lineNum, records, "line");
	local string retVal = TableReadFieldStr(lineTable, field, records[1]);
	return retVal;
}
# Get the value of the first record attached to the line "lineNum"
func readLineTableRecord(numeric lineNum, string field)
{
	local array numeric records[1];
	TableReadAttachment(lineTable, lineNum, records, "line");
	local numeric retVal = TableReadFieldNum(lineTable, field, records[1]);
	if (IsNull(retVal)) return 0;
	return retVal;
}
# Get the value of the first record attached to the line "nodeNum"
func readNodeTableRecord(numeric nodeNum, string field)
{
	local array numeric records[1];
	TableReadAttachment(nodeTable, nodeNum, records, "node");
	local numeric retVal = TableReadFieldNum(nodeTable, field, records[1]);
	return retVal;
}
# Get the z value
func computeElevationFromDEM(class POINT2D p)
{
	p = mapTrans.ConvertPoint2DFwd(p);	# convert from vector map to raster map coords
	local class POINT2D obj = MapToObject(GetLastUsedGeorefObject(dem), p.x, p.y, dem);
	obj.x = obj.x + .5; # center of cell
	obj.y = obj.y + .5; # center of cell
	local numeric demValue;
	if(isPointInRaster(obj.y, obj.x)) demValue = dem[obj.y, obj.x];	# y for line, x for column
	else demValue = null;
	return demValue;
}
# Find the closest line element using the polyline and the two nodes (vertices)
func findClosestLineElement(class POLYLINE origLine, numeric vertex1, numeric vertex2)
{
	# get the line given the two end points - straight line assumed
	local class POINT2D point1 = origLine.GetVertex(vertex1);
	local class POINT2D point2 = origLine.GetVertex(vertex2);
	local class POINT2D midPt = point1;
	midPt = midPt + point2;
	midPt.x = midPt.x / 2;
	midPt.y = midPt.y / 2;
	local numeric elemNum = FindClosestLine(lineVector, midPt.x, midPt.y);
	return elemNum;
}
# Interpolate between two points adding extra vertices
func class POLYLINE constructSmoothSurface(class POLYLINE p, numeric linscale, numeric colscale, numeric samplingRate)
{
	local class POLYLINE tmpLine, retLine;
	local class POINT2D point1, point2, tmpPt;
	local numeric numIntervals = 3;
	local numeric i;
	for (i=1; i<p.GetNumPoints(); i++)
	{
		point1 = p.GetVertex(i-1);
		point2 = p.GetVertex(i);
		local class POINT2D intervalPt = point2 - point1;
		intervalPt.x = intervalPt.x / (numIntervals+1);
		intervalPt.y = intervalPt.y / (numIntervals+1);
		if (intervalPt.x != 0 && intervalPt.y != 0)
		{
			tmpPt = point1;
			local numeric j;
			for (j=0; j<=numIntervals+1; j++)
			{
				tmpLine.AppendVertex(tmpPt);
				tmpPt = tmpPt + intervalPt;
			}
		}
	}
	local numeric distance=0;
	local numeric elevation = computeElevationFromDEM(tmpLine.GetVertex(0));
	tmpPt.x = distance;
	tmpPt.y = elevation;
	retLine.AppendVertex(tmpPt);
	for (i=1; i<tmpLine.GetNumPoints(); i++)
	{
		distance += computeDistance(tmpLine.GetVertex(i-1), tmpLine.GetVertex(i));
		elevation = computeElevationFromDEM(tmpLine.GetVertex(i));
		tmpPt.x = distance;
		tmpPt.y = elevation;
		retLine.AppendVertex(tmpPt);
	}
	return retLine;
}
# Construct the DEM surface elevation line, x dimension is line distance y is elevation
func class POLYLINE constructDemSurface(class POLYLINE demLine, class POLYLINE interpolated)
{
	local class POLYLINE retLine;
	if (demLine.GetNumPoints()<1) return retLine;
	local class POINT2D tmp;
	local numeric i;
	local numeric distance=0;
	local numeric elevation = computeElevationFromDEM(demLine.GetVertex(0));
	tmp.x = distance;
	tmp.y = elevation;
	retLine.AppendVertex(tmp);
	# get the elevation values from the raster
	for (i=1; i<demLine.GetNumPoints(); i++)
	{
		distance += computeDistance(demLine.GetVertex(i-1), demLine.GetVertex(i));
		elevation = computeElevationFromDEM(demLine.GetVertex(i));
		tmp.x = distance;
		tmp.y = elevation;
		retLine.AppendVertex(tmp);
	}
	return retLine;
}
# returns 1 if elemNum is reversed, 0 otherwise
func isLineReversed(numeric elemNum)
{
	return StrToNum(reversedList.GetString(indexOf(elemList, elemNum)));
}
# Construct the surface elevation line, x dimension is line distance y is elevation
func class POLYLINE constructSurface(class POLYLINE origLine)
{
	# DS == end node, US == start node
	local string pipeField = "CAN_I_US";
	local string surfField = "CAN_D_US";
	local class POLYLINE newLine;
	if (origLine.GetNumPoints()<1) return newLine;
	local class POINT2D tmp;
	local numeric i;
	local numeric distance=0;
	local numeric elemNum = findClosestLineElement(origLine, 0, 1);
	# reversed line 0 - can happen if first line is toggled off (not used as we flip bits in that case)
	if (isLineReversed(elemNum))
	{
		pipeField = "CAN_I_DS";
		surfField = "CAN_D_DS";
	}
	# Get elevation from start node
	local numeric elevation = readLineTableRecord(elemNum, pipeField) + readLineTableRecord(elemNum, surfField);
	if (!IsNull(elevation))
	{
		tmp.x = distance;
		tmp.y = elevation;
		newLine.AppendVertex(tmp);
	}
	# get the elevation values from the database table
	for (i=1; i<origLine.GetNumPoints(); i++)
	{
		local class POINT2D point1, point2;
		point1 = origLine.GetVertex(i-1);
		point2 = origLine.GetVertex(i);
		local numeric dx = computeDistance(point1, point2);
		distance += dx;
		# DS == end node, US == start node
		pipeField = "CAN_I_DS";
		surfField = "CAN_D_DS";
		local numeric k=0;
		# dx ==0 means we found a start node (thus US)
		if (dx==0)
		{
			k = 1;
			pipeField = "CAN_I_US";
			surfField = "CAN_D_US";
		}
		local numeric elemNum = findClosestLineElement(origLine, i-1+k, i+k);
		# check if the line was reversed - need to change field used
		if (isLineReversed(elemNum))
		{
			if (pipeField == "CAN_I_US") pipeField = "CAN_I_DS";
			else if (pipeField == "CAN_I_DS") pipeField = "CAN_I_US";
			if (surfField == "CAN_D_US") surfField = "CAN_D_DS";
			else if (surfField == "CAN_D_DS") surfField = "CAN_D_US";
		}
		# Get elevation from end node
		elevation = readLineTableRecord(elemNum, pipeField) + readLineTableRecord(elemNum, surfField);
		if (!IsNull(elevation))
		{
			tmp.x = distance;
			tmp.y = elevation;
			newLine.AppendVertex(tmp);
		}
	}
	return newLine;
}
# Construct the pipe face by creating a rectangle from four vertices of 'bottom' and 'top'
func class POLYLINE constructPipeFace(class POLYLINE bottom, class POLYLINE top)
{
	local class POLYLINE ret;
	if (bottom.GetNumPoints() != top.GetNumPoints())
	{
		numeric fillPipes = 0;
		return ret;
	}
	local numeric i;
	for (i=0; i<bottom.GetNumPoints(); i=i+2)
	{
		ret.AppendVertex(bottom.GetVertex(i));		# LL
		ret.AppendVertex(bottom.GetVertex(i+1));	# LR
		ret.AppendVertex(top.GetVertex(i+1));		# UR
		ret.AppendVertex(top.GetVertex(i));			# UL
		ret.AppendVertex(bottom.GetVertex(i));		# LL
	}
	return ret;
}
# Construct the pipe top - top.x = bottom.x && top.x = bottom.x + pipeHeight
func class POLYLINE constructPipeTop(class POLYLINE pipeBottom, class POLYLINE origLine)
{
	local class POLYLINE newLine;
	local class POINT2D tmp;
	# get the pipe top by calculating as heigth offset from bottom pipe line
	local numeric i;
	for (i=0; i<origLine.GetNumPoints(); i++)
	{
		local numeric vertex1, vertex2;
		if (i==0) vertex1 = i;
		else vertex1 = i-1;
		vertex2 = i;
		local class POINT2D p1 = origLine.GetVertex(vertex1);
		local class POINT2D p2 = origLine.GetVertex(vertex2);
		if (p1==p2)
		{
			vertex1++;
			vertex2++;
			if (i==0) vertex1 = i;
		}
		local numeric elemNum = findClosestLineElement(origLine, vertex1, vertex2);
		# Get elevation from end node
		local numeric elevation = readLineTableRecord(elemNum, "CAN_DIAM")/1000 + pipeBottom.GetVertex(i).y;
		if (!IsNull(elevation))
		{
			tmp.x = pipeBottom.GetVertex(i).x;
			tmp.y = elevation;
			newLine.AppendVertex(tmp);
		}
	}
	return newLine;
}
# Construct the graph line, x dimension is line distance y is elevation
func class STRINGLIST constructManholeNames(class POLYLINE origLine)
{
	local class STRINGLIST manholeNames;
	local class POINT2D prevPoint;
	# Draw all the manhole names at the manhole tops
	local numeric i;
	for (i=0; i<origLine.GetNumPoints(); i++)
	{
		# get the label
		local class POINT2D point1 = origLine.GetVertex(i);
		local string manholeLabel = "";
		if (point1!=prevPoint)
		{
			local numeric elemNum = FindClosestNode(lineVector, point1.x, point1.y);
			manholeLabel = readNodeTableRecordStr(elemNum, "MH_ID");
		}
		manholeNames.AddToEnd(manholeLabel);
		prevPoint = point1;
	}
	return manholeNames;
}
# Construct the graph line, x dimension is line distance y is elevation
func class POLYLINE constructManholeDepth(class POLYLINE pipeBottom, class POLYLINE origLine)
{
	local class POLYLINE newLine;
	local class POINT2D tmp;
	local numeric i;
	for (i=0; i<origLine.GetNumPoints(); i++)
	{
		local class POINT2D point1 = origLine.GetVertex(i);
		local numeric elemNum = FindClosestNode(lineVector, point1.x, point1.y);
		# Get elevation from end node
		local numeric elevation = readNodeTableRecord(elemNum, "MH_INVERT");
		tmp.x = pipeBottom.GetVertex(i).x;
		tmp.y = elevation;
		newLine.AppendVertex(tmp);
	}
	return newLine;
}
# Construct the graph line, x dimension is line distance y is elevation
func class POLYLINE constructPipeBottom(class POLYLINE origLine)
{
	local string field = "CAN_I_US";
	local class POLYLINE newLine;
	if (origLine.GetNumPoints()<1) return newLine;
	local class POINT2D tmp;
	local numeric i;
	local numeric distance=0;
	local numeric elemNum = findClosestLineElement(origLine, 0, 1);
	# check if the line 0 was reversed -- can happen if first line was toggled off (not used as we flip bits in that case)
	if (isLineReversed(elemNum))
	{
		field = "CAN_I_DS";
	}
	# Get elevation from start node
	local numeric elevation = readLineTableRecord(elemNum, field);
	if (!IsNull(elevation))
	{
		tmp.x = distance;
		tmp.y = elevation;
		newLine.AppendVertex(tmp);
	}
	for (i=1; i<origLine.GetNumPoints(); i++)
	{
		local class POINT2D point1, point2;
		point1 = origLine.GetVertex(i-1);
		point2 = origLine.GetVertex(i);
		local numeric dx = computeDistance(point1, point2);
		distance += dx;
		field = "CAN_I_DS";
		local numeric k=0;
		if (dx==0)
		{
			k = 1;
			field = "CAN_I_US";
		}
		local numeric elemNum = findClosestLineElement(origLine, i-1+k, i+k);
		# check if the line was reversed
		if (isLineReversed(elemNum))
		{
			if (field == "CAN_I_US") field = "CAN_I_DS";
			else if (field == "CAN_I_DS") field = "CAN_I_US";
		}
		# Get elevation from end node
		elevation = readLineTableRecord(elemNum, field);
		if (!IsNull(elevation))
		{
			tmp.x = distance;
			tmp.y = elevation;
			newLine.AppendVertex(tmp);
		}
	}
	return newLine;
}
# Determine if the value given is a null value
func isNull(numeric value)
{
#	return (value<=0);
	return (value < graphMinZ || value > graphMaxZ);
#	return (value == NullValue(dem));
}
# Get the width from the appropriate drawing device
func getHeight()
{
	return canvas.GetHeight();
}
# Get the width from the appropriate drawing device
func getWidth()
{
	return canvas.GetWidth();
}
# Get the extents of the graph
# (x1, y1) == UL corner
# (x2, y2) == LR corner
func class RECT getGraphExtents()
{
	local class RECT rect;
	rect.x1 = leftGraphOffset;
	rect.x2 = getWidth() - rightGraphOffset;
	rect.y1 = topGraphOffset;
	rect.y2 = getHeight() - bottomGraphOffset;
	return rect;
}
# Set the affine transformation for distance/elevation -> Graph Coords
proc setTrans(class POLYLINE graphLine)
{
	# copy the point
#	local class POINT2D retpoint;
#	retpoint = point;
#
#	# get graph extents
#	local numeric minx, maxx, miny, maxy;
#	minx = leftGraphOffset;
#	miny = topGraphOffset;
#	maxx = getWidth() - rightGraphOffset;
#	maxy = getHeight() - bottomGraphOffset;
#
#	# Get the drawing scale
#	local numeric xscale = 0, yscale = 0;
#	xscale = (maxx - minx) / abs(graphLine.GetVertex(graphLine.GetNumPoints()-1).x - graphLine.GetVertex(0).x);
#	if (graphMaxZ != graphMinZ) yscale = (maxy - miny) / (graphMaxZ - graphMinZ);
#
#	# apply scales and offsets
#	retpoint.x = point.x * xscale + leftGraphOffset;
#	if (yscale!=0) retpoint.y = getHeight() - ((point.y-graphMinZ) * yscale + bottomGraphOffset);
#	else retpoint.y = getHeight() - bottomGraphOffset;
	# redefinition here clears the trans each time we draw (which is desirable)
	class TRANSAFFINE trans;
	trans.ApplyScale(1,1);
	trans.ApplyOffset(0,0);
	# get graph extents
	local numeric minx, maxx, miny, maxy;
	minx = leftGraphOffset;
	miny = topGraphOffset;
	maxx = getWidth() - rightGraphOffset;
	maxy = getHeight() - bottomGraphOffset;
	# Get the drawing scale
	local numeric xscale = 0, yscale = 0;
	xscale = (maxx - minx) / abs(graphLine.GetVertex(graphLine.GetNumPoints()-1).x - graphLine.GetVertex(0).x);
	if (graphMaxZ != graphMinZ) yscale = (maxy - miny) / (graphMaxZ - graphMinZ);
	# apply scales and offsets
	trans.ApplyScale(xscale, -yscale);
	trans.ApplyOffset(leftGraphOffset, getHeight() + graphMinZ*yscale - bottomGraphOffset);
}
# Create the GC here using the appropriate drawing device (gc is global)
proc createGC()
{
	gc = canvas.CreateGC();
}
# draw the graph background fill
proc drawBackground(class GC gc, class COLOR bgcolor)
{
	if (drawable)
	{
		# draw the background rectangle
		gc.SetColorRGB(bgcolor.red, bgcolor.green, bgcolor.blue, 100);
		gc.FillRect(0, 0, getWidth(), getHeight());
	}
}
# draw the axes for the graph - dataMinZ == MAX_NUMBER and dataMaxZ == -MAX_NUMBER are displayed as null
proc drawGraphAxes(class GC gc, numeric distance, string xAxisLabel, string yAxisLabel, numeric drawTwoPointLines, class COLOR axiscolor, numeric fontHeight, numeric axisLabelOffset)
{
	if (drawable)
	{
		# set the axis colors
		gc.SetColorRGB(axiscolor.red, axiscolor.green, axiscolor.blue, 100);
		# set the min and max display variables
		local string min$ = sprintf("%0.2f", graphMinZ);
		local string max$ = sprintf("%0.2f", graphMaxZ);
		if (graphMaxZ==-MAX_NUMBER) max$ = "null";
		if (graphMinZ==MAX_NUMBER) min$ = "null";
		# Draw text for coordinate and elevation axis labels
		gc.DrawTextSetFont("ARIAL");
		gc.DrawTextSetHeightPixels(fontHeight);
		# Draw graph axes
		if (graphMaxZ == graphMinZ) max$ = "";
		local array numeric graphx[3], graphy[3];
		graphx[1] = leftGraphOffset;
		graphy[1] = topGraphOffset;
		graphx[2] = leftGraphOffset;
		graphy[2] = getHeight() - bottomGraphOffset;
		graphx[3] = getWidth() - rightGraphOffset;
		graphy[3] = graphy[2];
		gc.DrawPolyLine(graphx, graphy, 3);
		# draw y axis labels
		gc.TextStyle.Smoothing = 1;
		gc.DrawTextSimple(max$, 2, graphy[1]+fontHeight/2);
		gc.DrawTextSimple(min$, 2, graphy[2]+fontHeight/2);
		gc.DrawTextSimple(yAxisLabel, graphx[1]-axisLabelOffset, (graphy[3]-graphy[1])/2+gc.TextGetWidth(yAxisLabel)*1.5, 90);
		# draw x axis labels
		gc.DrawTextSimple("0", graphx[1] - gc.TextGetWidth("0")/2, getHeight()-3);
		local string str$ = sprintf("%0.2f", distance);
		gc.DrawTextSimple(str$, getWidth() - gc.TextGetWidth(str$), getHeight()-3);
		gc.DrawTextSimple(xAxisLabel, (graphx[3]-graphx[1])/2-gc.TextGetWidth(xAxisLabel)/4, graphy[3]+fontHeight+axisLabelOffset);
	}
}
# Translate a point on the graphline to image device coordinates for drawing
func class POINT2D transPointToGraph(class POINT2D point, class POLYLINE graphLine)
{
	local class POINT2D retpoint;
	if (trans!=0) retpoint = trans.ConvertPoint2DFwd(point);
	# copy the point
#	local class POINT2D retpoint;
#	retpoint = point;
#
#	# get graph extents
#	local numeric minx, maxx, miny, maxy;
#	minx = leftGraphOffset;
#	miny = topGraphOffset;
#	maxx = getWidth() - rightGraphOffset;
#	maxy = getHeight() - bottomGraphOffset;
#
#	# Get the drawing scale
#	local numeric xscale = 0, yscale = 0;
#	xscale = (maxx - minx) / abs(graphLine.GetVertex(graphLine.GetNumPoints()-1).x - graphLine.GetVertex(0).x);
#	if (graphMaxZ != graphMinZ) yscale = (maxy - miny) / (graphMaxZ - graphMinZ);
#
#	# apply scales and offsets
#	retpoint.x = point.x * xscale + leftGraphOffset;
#	if (yscale!=0) retpoint.y = getHeight() - ((point.y-graphMinZ) * yscale + bottomGraphOffset);
#	else retpoint.y = getHeight() - bottomGraphOffset;
	return retpoint;
}
# Plot the vertex and any connecting lines in the graphLine
# (note: this method is a bit more clever as it doesn't translate a point more than once
# but is not being used currently - it does make drawPolyline a bit less clear however).
proc plotLine(class GC gc, class POLYLINE graphLine, numeric vertexNum)
{
	local class POINT2D linePoint, graphPoint;
	linePoint = graphLine.GetVertex(vertexNum);
	graphPoint = transPointToGraph(linePoint, graphLine);
	if (0)#!isConsecutive(graphLine, vertexNum))
	{
		# if not consecutive, finish drawing and move to next
		gc.DrawTo(graphPoint.x, graphPoint.y);
		linePoint = graphLine.GetVertex(vertexNum+1);
		graphPoint = transPointToGraph(linePoint, graphLine);
		gc.MoveTo(graphPoint.x, graphPoint.y);
	}
	else if (isNull(linePoint.y))
	{
		# if null skip point and move to next
		linePoint = graphLine.GetVertex(vertexNum+1);
		graphPoint = transPointToGraph(linePoint, graphLine);
		gc.MoveTo(graphPoint.x, graphPoint.y);
	}
	else
	{
		gc.DrawTo(graphPoint.x, graphPoint.y);
	}
}
# draw a two point line from the polyline p
proc drawLineSegment(class POLYLINE p, numeric vertex1, numeric vertex2, class COLOR color)
{
	local class POINT2D point1, point2;
	point1 = p.GetVertex(vertex1);
	point2 = p.GetVertex(vertex2);
	if (isNull(point1.y)||isNull(point2.y))
	{
		# if null skip point and move to next
		point2 = transPointToGraph(point2, p);
		gc.MoveTo(point2.x, point2.y);
	}
	else
	{
		point1 = transPointToGraph(point1, p);
		point2 = transPointToGraph(point2, p);
		gc.SetColorRGB(color.red, color.green, color.blue, 100);
		gc.MoveTo(point1.x, point1.y);
		gc.DrawTo(point2.x, point2.y);
	}
}
# Draw the graph with the given polyline (not used currently - use with plotLine)
proc drawPolyline2(class GC gc, class POLYLINE graphLine, class COLOR lineColor)
{
	if (drawable && graphLine.GetNumPoints()>0)
	{
		# Draw the profile
		gc.SetColorRGB(lineColor.red, lineColor.green, lineColor.blue, 100);
		local class POINT2D linePoint = graphLine.GetVertex(0);
		# Plot point zero
		local class POINT2D graphPoint = transPointToGraph(linePoint, graphLine);
		gc.DrawPoint(graphPoint.x, graphPoint.y);
		# Plot the rest of the points - connecting them as lines
		local numeric i;
		for (i=0; i<graphLine.GetNumPoints(); i++)
		{
			plotLine(gc, graphLine, i);
		}
	}
}
# Draw the graph with the given polyline
proc drawPolyline1(class GC gc, class POLYLINE graphLine, class COLOR lineColor)
{
	if (drawable && graphLine.GetNumPoints()>0)
	{
		# Plot the polyline one segment at a time
		local numeric i;
		for (i=0; i<graphLine.GetNumPoints()-1; i++)
		{
			drawLineSegment(graphLine, i, i+1, lineColor);
		}
	}
}
# Draw the graph with the given polyline
proc drawPolyline(class GC gc, class POLYLINE graphLine, class COLOR lineColor)
{
	# Create a copy
	local class POLYLINE line = graphLine;
	line.ConvertForwardAffine(trans);
	# Set the line color
	gc.SetColorRGB(lineColor.red, lineColor.green, lineColor.blue, 100);
	# Draw it with gc method
	gc.DrawPolyLine2(line);
}
# Draw the graph with the given polyline as rectangles
# It is assumed that numPoints%5 = 0 (i.e. all rectangles are closed)
proc drawRectangles(class GC gc, class POLYLINE graphLine, class COLOR lineColor, class COLOR fillColor, numeric doFill)
{
	local class POLYLINE rectangle;
	if (drawable && graphLine.GetNumPoints()>=5)
	{
		local numeric i;
		for (i=0; i<graphLine.GetNumPoints(); i=i+5)
		{
			# Extract the rectangle and convert to graph coords
			graphLine.Extract(i, 5,rectangle);	# extract 5 pts (making up the closed rectangle)
			rectangle.ConvertForwardAffine(trans);
			# Set the line color
			gc.SetColorRGB(lineColor.red, lineColor.green, lineColor.blue, 100);
			gc.DrawPolyLine2(rectangle);
			# Set the fill color
			if (doFill)
			{
				gc.SetColorRGB(fillColor.red, fillColor.green, fillColor.blue, 100);
				gc.FillPolyLine2(rectangle);
			}
		}
	}
}
# Draw the names of the manholes
proc drawManholeNames(class GC gc, class POLYLINE surface, class STRINGLIST manholeNames, class COLOR textColor, string font, numeric pixelFontHeight)
{
	if (drawable && surface.GetNumPoints()>0)
	{
		gc.DrawTextSetFont(font);
		gc.DrawTextSetHeightPixels(pixelFontHeight);
		gc.DrawTextSetColors(textColor);
		# Draw all the manhole names at the manhole tops
		local numeric i;
		for (i=0; i<surface.GetNumPoints(); i++)
		{
			# get the position
			local class POINT2D linePoint = surface.GetVertex(i);
			if(!isNull(linePoint.y))
			{
				local class POINT2D topPoint = transPointToGraph(linePoint, surface);
				# draw the label
				string name = manholeNames.GetString(i);
				gc.TextStyle.Smoothing = 1;
				gc.DrawTextSimple(name, topPoint.x + pixelFontHeight, topPoint.y - 3, 90);
			}
		}
	}
}
# Draw the graph with the given polyline
proc drawManholes(class GC gc, class POLYLINE bottomLine, class POLYLINE surface, class COLOR lineColor)
{
	if (drawable && bottomLine.GetNumPoints()>0 && surface.GetNumPoints()>0)
	{
		# Draw the profile
		gc.SetColorRGB(lineColor.red, lineColor.green, lineColor.blue, 100);
		# Plot the rest of the points - connecting them as lines
		local numeric i;
		for (i=0; i<bottomLine.GetNumPoints(); i++)
		{
			local class POINT2D linePoint = bottomLine.GetVertex(i);
			local class POINT2D bottomPoint = transPointToGraph(linePoint, bottomLine);
			linePoint = surface.GetVertex(i);
			local class POINT2D topPoint = transPointToGraph(linePoint, bottomLine);
			# do the drawing
			gc.MoveTo(bottomPoint.x, topPoint.y);
			gc.DrawTo(bottomPoint.x, bottomPoint.y);
		}
	}
}
# Determine if the grid should be drawn or not
func doDrawGrid()
{
	return gridToggle.GetValue();
}
# Get the interval value for the x-dimension of the grid
func getGridIntervalX()
{
	# get the value entry then round or set appropriately
	local numeric xspacing = gridIntervalX.GetValue();
	if (abs(xspacing)<1)
	{
		xspacing=xspacing/abs(xspacing);
		gridIntervalX.SetValue(xspacing, 0);
	}
	else
	{
		xspacing = round(xspacing);
		gridIntervalX.SetValue(xspacing, 0);
	}
	return xspacing;
}
# Get the interval value for the y-dimension of the grid
func getGridIntervalY()
{
	# get the value entry then round or set appropriately
	local numeric yspacing = gridIntervalY.GetValue();
	if (abs(yspacing)<1)
	{
		yspacing=yspacing/abs(yspacing);
		gridIntervalY.SetValue(yspacing, 0);
	}
	else
	{
		yspacing = round(yspacing);
		gridIntervalY.SetValue(yspacing, 0);
	}
	return yspacing;
}
# Draw the grid
proc drawGrid(class GC gc, numeric xspacing, numeric yspacing, class POLYLINE bottomLine, class COLOR color)
{
	if (doDrawGrid() && drawable && bottomLine.GetNumPoints()>1)
	{
		# set the grid color
		gc.SetColorRGB(color.red, color.green, color.blue, 100);
		local class POINT2D pt1, pt2;
		# draw vertical lines
		local numeric length = bottomLine.GetVertex(bottomLine.GetNumPoints()-1).x;
		local class POINT2D bottomPoint;
		local class POINT2D topPoint;
		bottomPoint.y = graphMinZ;
		topPoint.y = graphMaxZ;
		for (bottomPoint.x = topPoint.x = xspacing; bottomPoint.x<=length; topPoint.x = bottomPoint.x = bottomPoint.x + xspacing)
		{
			# get graph coordinates
			local class POINT2D graphBottomPoint = transPointToGraph(bottomPoint, bottomLine);
			local class POINT2D graphTopPoint = transPointToGraph(topPoint, bottomLine);
			# do the drawing
			gc.MoveTo(graphTopPoint.x, graphTopPoint.y);
			gc.DrawTo(graphBottomPoint.x, graphBottomPoint.y);
		}
		# draw horizontal lines
		bottomPoint.x = 0;
		topPoint.x = length;
		bottomPoint.y = topPoint.y = graphMinZ;
		for (bottomPoint.y = topPoint.y = ceil(graphMinZ); bottomPoint.y<=graphMaxZ; topPoint.y = bottomPoint.y = bottomPoint.y + yspacing)
		{
			# get graph coordinates
			local class POINT2D graphBottomPoint = transPointToGraph(bottomPoint, bottomLine);
			local class POINT2D graphTopPoint = transPointToGraph(topPoint, bottomLine);
			# do the drawing
			gc.MoveTo(graphTopPoint.x, graphTopPoint.y);
			gc.DrawTo(graphBottomPoint.x, graphBottomPoint.y);
		}
	}
}
# Convert the polyline from obj to map coordinates
func class POLYLINE convertObjectToMap(class POLYLINE line)
{
	local class POLYLINE ret;
	local class POINT2D obj, map;
	local numeric i;
	# loop through the lines and convert from object to map coords
	for (i=0; i<line.GetNumPoints(); i++)
	{
		obj = line.GetVertex(i);
		map = ObjectToMap(lineVector, obj.x, obj.y, vecGeoref);
		ret.AppendVertex(map);
	}
	return ret;
}
# Determine if the polyline needs to be reversed
# (based on the start and end nodes of the lines)
func needToReverseLine(class POLYLINE origLine, class POLYLINE newLine, first, last)
{
	local class POINT2D point1, point2;
	# no reversal append to end - check first to avoid unnecessary flip (S2==E1)
	point1 = origLine.GetVertex(last);
	point2 = newLine.GetVertex(0);
	if (point1==point2) return 0;
	# no reversal append to front - check first to avoid unnecessary flip (S1==E2)
	point1 = origLine.GetVertex(first);
	point2 = newLine.GetVertex(newLine.GetNumPoints()-1);
	if (point1==point2) return 0;
	# reverse the new line (line2) for append to end (E1==E2)
	point1 = origLine.GetVertex(last);
	point2 = newLine.GetVertex(newLine.GetNumPoints()-1);
	if (point1==point2) return 1;
	# reverse the new line for append to front (S1==S2)
	point1 = origLine.GetVertex(first);
	point2 = newLine.GetVertex(0);
	if (point1==point2) return 1;
	# no reversal - all disjoint cases
	return 0;
}
# return 1 if should append polyline to end of line, 2 if should append polyline to front of line, 0 for no append
func doAppendEndPoints(class POINT2D start1, class POINT2D end1, class POINT2D start2, class POINT2D end2)
{
	if(end1 == start2) return 1;
	else if (end2 == start1) return 2;
	else return 0;
}
# return 1 if should append polyline to end of line, 2 if should append polyline to front of line, 0 for no append
func doAppend(class POLYLINE line, class POLYLINE polyline)
{
	class POINT2D lineStart = line.GetVertex(0);
	class POINT2D lineEnd = line.GetVertex(line.GetNumPoints()-1);
	class POINT2D polylineStart = polyline.GetVertex(0);
	class POINT2D polylineEnd = polyline.GetVertex(polyline.GetNumPoints()-1);
	return doAppendEndPoints(lineStart, lineEnd, polylineStart, polylineEnd);
}
# append the 'lineToAdd' to the 'line'
proc appendLine(class POLYLINE line, numeric newLine, numeric twoPointLine, numeric orderElements)
{
	# Get the line as a polyline
	local class POLYLINE polyline;
	polyline = GetVectorLine(lineVector, newLine);
	polyline = convertObjectToMap(polyline);
	if (twoPointLine)
	{
		if (polyline.GetNumPoints()>2) polyline.Straighten();
	}
	# if this is the first line just append and return
	if (line.GetNumPoints()==0)
	{
		line.Append(polyline);
		if (orderElements) orderedElemList.AddToFront(NumToStr(newLine));
		return;
	}
	# Check to see if we need to reverse the polyline
	local numeric reverse = needToReverseLine(line, polyline, 0, line.GetNumPoints()-1);
	if (reverse==1)
	{
		polyline.Reverse();
		reversedList.SetString("1", indexOf(elemList ,newLine));
	}
	# now simply append the line
	if (doAppend(line, polyline)==1)
	{
		# append to end
		line.Append(polyline);
		if (orderElements) orderedElemList.AddToEnd(NumToStr(newLine));
	}
	else if (doAppend(line, polyline)==2)
	{
		# append to front
		polyline.Append(line);
		line.Clear();
		line.Append(polyline);
		if (orderElements) orderedElemList.AddToFront(NumToStr(newLine));
	}
	else 
	{
		# can occur if loop is made, should clear selection
		PopupMessage("Cannot append disjoint line");
	}
}
# set the global graph offsets
proc setGraphOffsets(class GC gc, numeric fontHeight, numeric axisLabelOffset)
{
	# set the min and max display variables
	local string min$ = sprintf("%0.2f", graphMinZ);
	local string max$ = sprintf("%0.2f", graphMaxZ);
	if (graphMaxZ==-MAX_NUMBER) max$ = "null";
	if (graphMinZ==MAX_NUMBER) min$ = "null";
	# PopupMessage( sprintf("font height = %d", NumToStr(fontHeight) ) );
	if (graphMaxZ == graphMinZ) max$ = "";
	local numeric size = max(gc.TextGetWidth(max$), gc.TextGetWidth(min$));
	# PopupMessage( sprintf("max$ width = %d, min$ width = %d, size = %d", gc.TextGetWidth(max$), gc.TextGetWidth(min$), size) );
	size = max(size, fontHeight+axisLabelOffset);
	leftGraphOffset = size - 4;
	# PopupMessage( sprintf("size = %d, height+offset = %d, leftGraphOffset = %d", size, fontHeight+axisLabelOffset, leftGraphOffset) );
}
# Procedure used to draw the graph; passed a special GC
# created by the following procedure.
proc drawGraph()
{
	# save pipe top and bottom for highlighting
	pipeBottomSave = pipeBottom;
	pipeTopSave = pipeTop;
	pipeFaceSave = pipeFace;
	# create graphics context for graph
	createGC();
	gc.DrawTextSetFont("ARIAL");
	local class COLOR color;
	# set up the affine transformation for the graph
	setTrans(pipeBottom);
	# set up graph axes
	local string xlabel = "Distance (m)";
	local string ylabel = "Elevation (m)";
	local numeric drawTwoPointLines = 0;
	local numeric drawStartEndPoints = 1;
	# set the graph offsets - (globals)
	local numeric fontHeight = 12, axisLabelOffset = 3;
	setGraphOffsets(gc, fontHeight, axisLabelOffset);
	# fill in the background
	local class COLOR bgcolor;
	bgcolor.red = 98; bgcolor.green = 98; bgcolor.blue = 98;
	drawBackground(gc, bgcolor);
	# draw the grid
	color.red = 80; color.green = 80; color.blue = 80;
	drawGrid(gc, getGridIntervalX(), getGridIntervalY(), pipeBottom, color);
	# Draw and label the axes
	color.red = 0; color.green = 0; color.blue = 0;
	drawGraphAxes(gc, pipeBottom.GetVertex(pipeBottom.GetNumPoints()-1).x, xlabel, ylabel, drawTwoPointLines, color, fontHeight, axisLabelOffset);
	# draw the pipe bottom
	color = vectorLayer.SelectedElemColor;
#	drawPolyline(gc, pipeBottom, color);
	# draw the pipe top
#	drawPolyline(gc, pipeTop, color);
	# draw the pipe face
	local class COLOR fill = vectorLayer.SelectedElemColor;
#	fill.red = 40; fill.green = 40; fill.blue = 80;
	drawRectangles(gc, pipeFace, color, fill, fillToggle.GetValue());
	# draw the surface line (as DEM or from DB)
	class POLYLINE manholeSurfaceLine;
	if (doUseDEM())
	{
		# draw smoothed DEM surface line
		gc.DrawSetLineStyle("");
		color.red = 0; color.green = 0; color.blue = 0;
		drawPolyline(gc, smoothedSurface, color);
		manholeSurfaceLine = demSurface;
	}
	else
	{
		# draw surface line
		color.red = 0; color.green = 0; color.blue = 0;
		drawPolyline(gc, surface, color);
		manholeSurfaceLine = surface;
	}
	# draw the manholes
	color.red = 20; color.green = 80; color.blue = 20;
	drawManholes(gc, manholeDepth, manholeSurfaceLine, color);
	# draw the manhole labels
	color.red = 0; color.green = 0; color.blue = 0;
	drawManholeNames(gc, manholeSurfaceLine, manholeNames, color, "ARIAL", 12);
	canvas.Refresh(1);	# refresh the drawing canvas
}
# Procedure for drawing graph in CAD object; passed a local GC 
# when called by the OnSaveGraph() procedure
proc drawGraphForCAD(class GC gc)
{
	local class COLOR color;
	gc.DrawTextSetFont("ARIAL");
	# set up the affine transformation for the graph
	setTrans(pipeBottom);
	# set up graph axes
	local string xlabel = "Distance (m)";
	local string ylabel = "Elevation (m)";
	local numeric drawTwoPointLines = 0;
	local numeric drawStartEndPoints = 1;
	# set the graph offsets - (globals)
	local numeric fontHeight = 12, axisLabelOffset=3;
	setGraphOffsets(gc, fontHeight, axisLabelOffset);
	# fill in the background
	local class COLOR bgcolor;
	bgcolor.red = 98; bgcolor.green = 98; bgcolor.blue = 98;
	drawBackground(gc, bgcolor);
	# draw the grid
	color.red = 80; color.green = 80; color.blue = 80;
	drawGrid(gc, getGridIntervalX(), getGridIntervalY(), pipeBottom, color);
	# Draw and label the axes
	color.red = 0; color.green = 0; color.blue = 0;
	drawGraphAxes(gc, pipeBottom.GetVertex(pipeBottom.GetNumPoints()-1).x, xlabel, ylabel, drawTwoPointLines, color, fontHeight, axisLabelOffset);
	# draw the pipe bottom
	color = vectorLayer.SelectedElemColor;
#	drawPolyline(gc, pipeBottom, color);
	# draw the pipe top
#	drawPolyline(gc, pipeTop, color);
	# draw the pipe face
	local class COLOR fill = vectorLayer.SelectedElemColor;
#	fill.red = 40; fill.green = 40; fill.blue = 80;
	drawRectangles(gc, pipeFace, color, fill, fillToggle.GetValue());
	# draw the surface line (as DEM or from DB)
	class POLYLINE manholeSurfaceLine;
	if (doUseDEM())
	{
		# draw smoothed DEM surface line
		gc.DrawSetLineStyle("");
		color.red = 0; color.green = 0; color.blue = 0;
		drawPolyline(gc, smoothedSurface, color);
		manholeSurfaceLine = demSurface;
	}
	else
	{
		# draw surface line
		color.red = 0; color.green = 0; color.blue = 0;
		drawPolyline(gc, surface, color);
		manholeSurfaceLine = surface;
	}
	# draw the manholes
	color.red = 20; color.green = 80; color.blue = 20;
	drawManholes(gc, manholeDepth, manholeSurfaceLine, color);
	# draw the manhole labels
	color.red = 0; color.green = 0; color.blue = 0;
	drawManholeNames(gc, manholeSurfaceLine, manholeNames, color, "ARIAL", 12);
}
# Procedure called when Save Graph as CAD button on dialog is pressed
proc OnSaveGraph() 
{
	GetOutputCAD(CADgraph);	# prompt user to select output CAD object
	# create GREDEVICE for drawing into CAD object
	deviceCAD.Create(CADgraph , getHeight(), getWidth() );
	# create graphics context for the device
	local class GC gcCAD;
	gcCAD = deviceCAD.CreateGC();
	# call procedure to draw the graph for the CAD object
	drawGraphForCAD(gcCAD); 
	deviceCAD.Close();	# close CAD graphics device
	CloseCAD(CADgraph);	# close CAD object
}
# Called when the min or max z value is changed
proc OnChangeGraphZ()
{
	graphMinZ = graphMinSetting.GetValue();
	graphMaxZ = graphMaxSetting.GetValue();
	needsRedraw = 1;
}
# Callback for a right mouse button press
proc OnRightButtonPress()
{
	# make sure z settings are current
	OnChangeGraphZ();
	# clear the line
	demLine.Clear();
	orderedElemList.Clear();
	# Get the first line as a polyline
	class POLYLINE finalLine;
	# Get all of the lines, appending appropriately
	local numeric i;
	for (i=0; i<elemList.GetNumItems(); i++)
	{
		appendLine(finalLine, getElement(elemList, i), 1, 1);
		appendLine(demLine, getElement(elemList, i), 0, 0);
	}
	# Get the line to graph - x-dimension is distance, y is elevation
	pipeBottom = constructPipeBottom(finalLine);
	# Create the pipe top line - bottom + pipeHeight
	pipeTop = constructPipeTop(pipeBottom, finalLine);
	# Create the pipe face - defines the area between the pipeBottom and pipeTop
	pipeFace = constructPipeFace(pipeBottom, pipeTop);
	# Create the surface line
	surface = constructSurface(finalLine);
	# Create the manhole depth line
	manholeDepth = constructManholeDepth(pipeBottom, finalLine);
	# Create the manhole labels
	manholeNames = constructManholeNames(finalLine);
	# Create the dem surface line
	if (doUseDEM() && demSet)
	{
		demSurface = constructDemSurface(demLine);
		smoothedSurface = constructSmoothSurface(demLine, LinScale(dem), ColScale(dem), 3);
	}
	drawable = 1;
	currentlyActive = -1;
	drawGraph();
	needsRedraw=0;
}
# Return a number > 0 if one of the two nodes matches an end node.
# case 1: node1==endNodes[1] return 1
# case 2: node1==endNodes[2] return 2
# case 3: node2==endNodes[1] return 3
# case 4: node2==endNodes[2] return 4
func matchesEndNodes(numeric node1, numeric node2)
{
	if (node1==endNodes[1])	# S1 == S2
	{
		endNodes[1] = node2;
		return 1;
	}
	if (node1==endNodes[2])	# E1 == S2
	{
		endNodes[2] = node2;
		return 2;
	}
	if (node2==endNodes[1])	# S1 == E2
	{
		endNodes[1] = node1;
		return 3;
	}
	if (node2==endNodes[2])	# E1 == E2
	{
		endNodes[2] = node1;
		return 4;
	}
	return 0;
}
# if list has 0 -> 1 else -> 0
proc reverseBits(class STRINGLIST s)
{
	local numeric i;
	for (i=0; i<s.GetNumItems(); i++)
	{
		if (s.GetString(i)=="0") s.SetString("1", i);
		else s.SetString("0", i);
	}
}
# Toggle the line element given the element number
proc toggleClosestLineElement(numeric elemNum)
{
	# check the endnodes for a match
	local numeric node1 = lineVector.LINE[elemNum].Internal.StartNode;
	local numeric node2 = lineVector.LINE[elemNum].Internal.EndNode;
	local numeric index = indexOf(elemList, elemNum);
	local numeric matchNum = matchesEndNodes(node1, node2);
	if (index>=0)
	{
		local numeric doRemove = 0;
		# check to see if the current line is removable
		if (matchNum>0) doRemove = 1;
		if (doRemove)
		{
			# remove the line
			vectorLayer.Line.HighlightSingle(elemNum, "Subtract");
			elemList.Remove(index);
			reversedList.Remove(index);
			if (reversedList.GetString(0)=="1") reverseBits(reversedList);
			if(vectorLayer.Line.GetSelectedElement()==0)
			{
				endNodes[1] = -1;
				endNodes[2] = -1;
			}
		}
	}
	else
	{
		# if no lines exist, then simply append
		if (endNodes[1]==-1 && endNodes[2]==-1)
		{
			endNodes[1] = node1;
			endNodes[2] = node2;
			vectorLayer.Line.HighlightSingle(elemNum, "Add");
			elemList.AddToEnd(NumToStr(elemNum));
			reversedList.AddToEnd("0");
		}
		else if (matchNum>0) # check to see if the current line is appendable
		{
			# add the line
			vectorLayer.Line.HighlightSingle(elemNum, "Add");
			elemList.AddToEnd(NumToStr(elemNum));
			reversedList.AddToEnd("0");
		}
	}
}
# Toggle the line element closest to the point 'position'
func toggleClosestLine(class POINT2D position)
{
	local numeric elemNum = FindClosestLine(lineVector, position.x, position.y);
	if (elemNum > 0 )
	{
		toggleClosestLineElement(elemNum);
	}
	return elemNum;
}
# Callback for a left mouse button press
proc OnLeftButtonPress()
{
	# Find cursor position in screen coordinates
	local class POINT2D point;
	point.x = PointerX;
	point.y = PointerY;
	point = TransPoint2D(point, ViewGetTransLayerToScreen(View, vectorLayer, 1));
	# toggle the element as selected (if valid) or deselected (if possible)
	toggleClosestLine(point);
}
# Called when the close button is pressed.  Closes the dialogs.
proc cbClose()
{
	dlgwin.Close(0);
	if (setDefaultWhenClose)
	{
		setDefaultWhenClose = false;
		View.SetDefaultTool();
	}
}
# Called when the canvas is resized - gc is recreated before drawing
proc OnCanvasResize(class GUI_CANVAS canvas, numeric width, numeric height)
{
	OnRightButtonPress();
}
# return the element number of the nearest line to the graph point
func getNearestLineElementFromGraph(class POINT2D graphPoint)
{
	# make sure z settings are current
#	graphMinZ = graphMinSetting.GetValue();
#	graphMaxZ = graphMaxSetting.GetValue();
	local class POLYLINE splits;
	local class POINT2D tmp;
	local numeric distance = 0;
	tmp.x = distance;
	tmp.y = 0;
	splits.AppendVertex(tmp);
	local numeric i=0;
	for (i=1; i<finalLine.GetNumPoints(); i++)
	{
		local class POINT2D point1, point2;
		point1 = finalLine.GetVertex(i-1);
		point2 = finalLine.GetVertex(i);
		local numeric dx = computeDistance(point1, point2);
		distance += dx;
		tmp.x = distance;
		tmp.y = 0;
		splits.AppendVertex(tmp);
	}
	local class RECT rect = getGraphExtents();
	# Get the drawing scale
	local numeric xscale = 0;
	xscale = (rect.x2 - rect.x1) / distance;
	# apply scales and offsets to get graph coords
	for (i=1; i<splits.GetNumPoints(); i++)
	{
		tmp = splits.GetVertex(i);
		tmp.x = tmp.x * xscale + leftGraphOffset;
		splits.SetVertex(i, tmp);
	}
	graphPoint.y = 0; # ignore y axis
	local numeric vertexNum = splits.FindClosestVertex(graphPoint);
	tmp = splits.GetVertex(vertexNum);
	# take care of duplicate vertex locations (which will choose first one)
	if (graphPoint.x > tmp.x)
	{
		vertexNum++;
	}
	if (vertexNum >= splits.GetNumPoints() || (vertexNum==1 && graphPoint.x < leftGraphOffset))
	{
		return 0;
	}
	# get the line from the vertex
	local numeric lineNum = floor(vertexNum/2);
	local numeric elemNum = getElement(orderedElemList, lineNum);
#	vertexDisplay.SetValue(vertexNum, 0);
#	lineDisplay.SetValue(lineNum, 0);
#	elemDisplay.SetValue(elemNum, 0);
	return elemNum;
}
proc highlightGraphSegment(numeric lineNum, class COLOR color)
{
	local class POLYLINE myrect;
	pipeFaceSave.Extract(lineNum*5, 5, myrect);
	drawRectangles(gc, myrect, color, color, fillToggle.GetValue());
	# draw the manholes
	color.red = 20; color.green = 80; color.blue = 20;
	drawManholes(gc, pipeBottomSave, pipeTopSave, color);
	canvas.Refresh(1);
}
proc makeLineActive(numeric elemNum)
{
	vectorLayer.Line.SetActiveElement(elemNum);
	local numeric lineOrdering = indexOf(orderedElemList, elemNum);
	if (lineOrdering>-1 && currentlyActive != lineOrdering)
	{
		local class COLOR color = vectorLayer.SelectedElemColor;
		if(currentlyActive>-1) highlightGraphSegment(currentlyActive, color);
		currentlyActive = lineOrdering;
#color.red = 90; color.green = 90; color.blue = 100;
#highlightGraphSegment(currentlyActive, color);
		color = vectorLayer.ActiveElemColor;
		highlightGraphSegment(currentlyActive, color);
		materialDisplay.SetValue(readLineTableRecordStr(elemNum, "CAN_MAT"), 0);
		slopeDisplay.SetValue(readLineTableRecord(elemNum, "CAN_SLOPE"), 0);
		diameterDisplay.SetValue(readLineTableRecord(elemNum, "CAN_DIAM")/1000, 0);
	}
}
func getNearestLineElementFromGraph2(class POINT2D graphPoint)
{
	# make sure z settings are current
#	graphMinZ = graphMinSetting.GetValue();
#	graphMaxZ = graphMaxSetting.GetValue();
	local class POLYLINE splits = pipeBottomSave;
	local class POINT2D tmp;
#	local numeric distance = 0;
#	tmp.x = distance;
#	tmp.y = 0;
#	splits.AppendVertex(tmp);
#
#	local numeric i=0;
#	for (i=1; i<finalLine.GetNumPoints(); i++)
#	{
#		local class POINT2D point1, point2;
#		point1 = finalLine.GetVertex(i-1);
#		point2 = finalLine.GetVertex(i);
#		local numeric dx = computeDistance(point1, point2);
#		distance += dx;
#
#		tmp.x = distance;
#		tmp.y = 0;
#		splits.AppendVertex(tmp);
#	}
#
#	local class RECT rect = getGraphExtents();
#
#	# Get the drawing scale
#	local numeric xscale = 0;
#	xscale = (rect.x2 - rect.x1) / distance;
	# apply scales and offsets to get graph coords
	local numeric i;
	for (i=1; i<splits.GetNumPoints(); i++)
	{
#		tmp = splits.GetVertex(i);
#		tmp.x = tmp.x * xscale + leftGraphOffset;
		tmp = trans.ConvertPoint2DFwd(splits.GetVertex(i));
		tmp.y = 0;
		splits.SetVertex(i, tmp);
	}
	graphPoint.y = 0; # ignore y axis
	local numeric vertexNum = splits.FindClosestVertex(graphPoint);
	tmp = splits.GetVertex(vertexNum);
	# take care of duplicate vertex locations (which will choose first one)
	if (graphPoint.x > tmp.x)
	{
		vertexNum++;
	}
	if (vertexNum >= splits.GetNumPoints() || (vertexNum==1 && graphPoint.x < leftGraphOffset))
	{
		return 0;
	}
	# get the line from the vertex
	local numeric lineNum = floor(vertexNum/2);
	local numeric elemNum = getElement(orderedElemList, lineNum);
#	vertexDisplay.SetValue(vertexNum, 0);
#	lineDisplay.SetValue(lineNum, 0);
#	elemDisplay.SetValue(elemNum, 0);
	return elemNum;
}
# Called when the mouse is moved over the canvas - highlights the nearest line in the 2d view
proc OnCanvasMouseMove(class GUI_CANVAS canvas, class POINT2D point, numeric shift, numeric ctrl)
{
#<debug>
local class POINT2D mapPoint = trans.ConvertPoint2DInv(point);
if (!IsNull(mapPoint.x) && !IsNull(mapPoint.y))
{
	mouseXDisplay.SetValue(mapPoint.x,0);
	mouseYDisplay.SetValue(mapPoint.y,0);
}
#</debug>
	if (needsRedraw)
	{
		OnRightButtonPress();
	}
	local numeric elemNum = getNearestLineElementFromGraph(point);
	if (elemNum > 0)
	{
		makeLineActive(elemNum);
	}
	needsRedraw = 0;
}
# Called when mouse is moved over the 2D view - highlights the nearest line in the 2d view
proc OnPointerMoveNoButton()
{
	if (needsRedraw)
	{
		OnRightButtonPress();
		needsRedraw = 0;
	}
	local class POINT2D pointer;
	pointer.x = PointerX;
	pointer.y = PointerY;
	# Get the cursor position in map coords
	local class TRANSPARM screenToView = ViewGetTransViewToScreen(View, 1);
	local class TRANSPARM viewToLayer = ViewGetTransLayerToView(View, vectorLayer, 1);
	pointer = TransPoint2D(pointer, screenToView);
	pointer = TransPoint2D(pointer, viewToLayer);
	local numeric elemNum = FindClosestLine(lineVector, pointer.x, pointer.y);
	if (elemNum > 0)
	{
		makeLineActive(elemNum);
	}
}
# Called when the button to clear the lines is pressed
proc OnClearLines()
{
	endNodes[1]=-1;
	endNodes[2]=-1;
	elemList.Clear();
	orderedElemList.Clear();
	reversedList.Clear();
	vectorLayer.UnhighlightAllElements(1);
	materialDisplay.SetValue("", 0);
	slopeDisplay.SetValue(0, 0);
	diameterDisplay.SetValue(0, 0);
	mouseXDisplay.SetValue(0, 0);
	mouseYDisplay.SetValue(0, 0);
	OnRightButtonPress();
}
# Called with the grid toggle button is pressed, does a full redraw
proc OnGridTogglePressed()
{
	OnRightButtonPress();
}
# Called with the fill toggle button is pressed, does a full redraw
proc OnFillTogglePressed()
{
	OnRightButtonPress();
}
# Called with the grid toggle button is pressed, checks that dem exists
proc OnDemTogglePressed()
{
	checkLayer();
}
# Called the first time the tool is activated.
# If the tool implements a dialog it should be created (but not displayed) here.
func OnInitialize()
{
	# initialize the end node values, used to validate line appends
	endNodes[1]=-1;
	endNodes[2]=-1;
	# handle as layout or as group
	if (Layout)
	{
		WidgetAddCallback(Layout.GroupSelectedCallback, cbGroup);
		activegroup = Layout.ActiveGroup;
	}
	else activegroup = Group;
	WidgetAddCallback(activegroup.LayerSelectedCallback, cbLayer);
	# define the dialog here with xml specification
	string xml$ = 
	'<?xml version="1.0"?>
	<!DOCTYPE root SYSTEM "smlforms.dtd">
	<root>
		<dialog id="guicanvas" Title="Line Profile" Buttons="">
			<groupbox Name=" Graph elevation display settings: " ExtraBorder="4" HorizResize="Fixed" VertResize="Fixed">
				<pane Orientation="horizontal">
					<togglebutton id="demtoggle" Name="Use DEM"/>
					<pane ChildSpacing="0" VertAlign="Top">
						<label>Minimum Z:</label>
						<editnumber id="minz" BlankZero="false" Precision="0" ReadOnly="false" Width="5" Height="10" Default="0" OnChanged="OnChangeGraphZ()"/>
					</pane>
					<pane ChildSpacing="0" VertAlign="Bottom">
						<label>Maximum Z:</label>
						<editnumber id="maxz" BlankZero="false" Precision="0" ReadOnly="false" Width="5" Height="10" Default="25" OnChanged="OnChangeGraphZ()"/>
					</pane>
				</pane>
			</groupbox>
			<groupbox Name=" Grid display settings: " ExtraBorder="4" HorizResize="Fixed" VertResize="Fixed">
				<pane Orientation="horizontal">
					<togglebutton id="gridtoggle" Name="Draw Grid"/>
					<pane ChildSpacing="0" VertAlign="Top">
						<label>X-Interval:</label>
						<editnumber id="xinterval" BlankZero="false" Precision="0" ReadOnly="false" Width="5" Height="10" Default="10"/>
					</pane>
					<pane ChildSpacing="0" VertAlign="Bottom">
						<label>Y-Interval:</label>
						<editnumber id="yinterval" BlankZero="false" Precision="0" ReadOnly="false" Width="5" Height="10" Default="1"/>
					</pane>
				</pane>
			</groupbox>
			<pane>
				<canvas id="canvas" ExtraBorder="4" Width="400" Height="300"/>
			</pane>
			<groupbox Name=" Pipe Attributes: " ExtraBorder="4" HorizResize="Fixed" VertResize="Fixed">
				<pane Orientation="horizontal" HorizAlign="Center">
					<pane ChildSpacing="0" HorizAlign="Left">
						<label>Pipe Material:</label>
						<edittext id="material" ReadOnly="true" Width="5"/>
					</pane>
					<pane ChildSpacing="0" HorizAlign="Left">
						<label>Pipe Diameter:</label>
						<editnumber id="diameter" BlankZero="true" Precision="3" ReadOnly="true" Width="5" Height="10" Default="0"/>
						<label>meters</label>
					</pane>
					<pane ChildSpacing="0" HorizAlign="Left">
						<label>Pipe Slope:</label>
						<editnumber id="slope" BlankZero="false" Precision="2" ReadOnly="true" Width="5" Height="10" Default="0"/>
					</pane>
				</pane>
			</groupbox>
			<pane Orientation="horizontal" HorizAlign="Center">
				<groupbox Name=" Coordinate Display: " ExtraBorder="4" HorizResize="Fixed" VertResize="Fixed">
					<pane Orientation="horizontal" HorizAlign="Center">
						<pane ChildSpacing="0" HorizAlign="Left">
							<label>Distance:</label>
							<editnumber id="mousex" BlankZero="false" Precision="2" ReadOnly="true" Width="5" Height="10" Default="0" MinVal="0" />
						</pane>
						<pane ChildSpacing="0" HorizAlign="Left">
							<label>Elevation:</label>
							<editnumber id="mousey" BlankZero="false" Precision="2" ReadOnly="true" Width="5" Height="10" Default="0"/>
						</pane>
					</pane>
				</groupbox>
				<groupbox Name=" Pipe Style Settings: " ExtraBorder="4" HorizResize="Fixed" VertResize="Fixed">
					<pane Orientation="horizontal">
						<togglebutton id="filltoggle" Name="Fill Pipe Polygons"/>
					</pane>
				</groupbox>
			</pane>
<!--
				<pane Orientation="horizontal" HorizAlign="Center">
					<pane ChildSpacing="0" HorizAlign="Left">
						<label>VertexNum:</label>
						<editnumber id="vertex" BlankZero="false" Precision="0" ReadOnly="true" Width="5" Height="10" Default="0"/>
					</pane>
					<pane ChildSpacing="0" HorizAlign="Left">
						<label>LineNum:</label>
						<editnumber id="line" BlankZero="false" Precision="0" ReadOnly="true" Width="5" Height="10" Default="0"/>
					</pane>
					<pane ChildSpacing="0" HorizAlign="Left">
						<label>ElemNum:</label>
						<editnumber id="elem" BlankZero="true" Precision="0" ReadOnly="true" Width="5" Height="10" Default="0"/>
					</pane>
				</pane>
-->
			<pane Orientation="horizontal" HorizAlign="Center" HorizResize="Fixed" VertResize="Fixed">
				<pushbutton id="clearlines" Name="Clear Selected Lines" OnPressed="OnClearLines()"/>
				<pushbutton id="saveGraph" Name="Save Graph as CAD" OnPressed="OnSaveGraph()"/>
			</pane>
		</dialog>
	</root>
	';
	# Parse the xml dialog specification
	local class XMLDOC dlgdoc;
	local numeric err = dlgdoc.Parse(xml$);
	if (err < 0) {
		PopupError(err);
		Exit();
		}
	local string dlgid$ = "guicanvas";
	local class XMLNODE dlgnode = dlgdoc.GetElementByID(dlgid$);
	if (dlgnode == 0) {
		PopupMessage("Could not find specified id: "+dlgid$);
		Exit();
		}
	dlgwin.SetXMLNode(dlgnode);
	# create the dialog as a modeless dialog
	err = dlgwin.CreateModeless();
	if (err < 0) {
		PopupError(err);
		Exit();
		}
	# get the control for the drawing canvas
	canvas = dlgwin.GetCtrlByID("canvas");
	canvas.SetOnSize(OnCanvasResize);
	canvas.SetOnRightDown(OnCanvasResize);
	canvas.SetOnMouseMove(OnCanvasMouseMove);
	# get the control for the grid toggle
	gridToggle = dlgwin.GetCtrlByID("gridtoggle");
	gridToggle.SetValue(1,0);
	gridToggle.SetOnPressed(OnGridTogglePressed);
	# get the control for the dem toggle
	demToggle = dlgwin.GetCtrlByID("demtoggle");
	demToggle.SetOnPressed(OnDemTogglePressed);
	# get the controls for the grid interval settings
	gridIntervalX = dlgwin.GetCtrlByID("xinterval");
	gridIntervalY = dlgwin.GetCtrlByID("yinterval");
	# get the controls for the min and max z setting
	graphMinSetting = dlgwin.GetCtrlByID("minz");
	graphMaxSetting = dlgwin.GetCtrlByID("maxz");
	OnChangeGraphZ();
	# get the control for the grid toggle
	fillToggle = dlgwin.GetCtrlByID("filltoggle");
	fillToggle.SetValue(1,0);
	fillToggle.SetOnPressed(OnFillTogglePressed);
	# get control for display of info
#	vertexDisplay = dlgwin.GetCtrlByID("vertex");
#	lineDisplay = dlgwin.GetCtrlByID("line");
#	elemDisplay = dlgwin.GetCtrlByID("elem");
	mouseXDisplay = dlgwin.GetCtrlByID("mousex");
	mouseYDisplay = dlgwin.GetCtrlByID("mousey");
	materialDisplay = dlgwin.GetCtrlByID("material");
	slopeDisplay = dlgwin.GetCtrlByID("slope");
	diameterDisplay = dlgwin.GetCtrlByID("diameter");
	drawable = 0;
}
 
# Called when tool is to be destroyed, will not be called if tool was never activated.
func OnDestroy()
{
	dlgwin.Close(0);
}
# Called when tool is activated.
func OnActivate()
{
	checkLayer();
	# open the graph dialog window
	dlgwin.Open();
	# draw the graph
	OnRightButtonPress();
	setDefaultWhenClose = true;
}
 
# Called when tool is deactivated (usually when switching to another tool).
func OnDeactivate()
{
	setDefaultWhenClose = false;
	cbClose();
}