I am trying to draw an image originating from a list of X,Y values that represent the start and stop points of a line. They are in inches, so they are currently formatted in to decimals.
The problem I am having is with the drawing. The MoveTo
and LineTo
commands require an integer not a double. If I use the Round(float)
math operation, you see the output below. The rounding results in the same start and stop point, so nothing is drawn.
How can I draw my shape from a list of decimal X,Y points?
Debug code for input values (decimals):
LineStartVal: -88.988857, 36.265838
LineEndVal: -89.094923, 36.371904
LineStartVal: -89.094923, 36.371904
LineEndVal: -95.000423, 36.371904
LineStartVal: -95.000423, 36.371904
LineEndVal: -95.000423, 32.828604
LineStartVal: -95.000423, 32.828604
LineEndVal: -99.134273, 32.828604
Debug code for output points after rounding:
MoveTo: -89, 36
LineTo: -89, 36
MoveTo: -89, 36
LineTo: -95, 36
MoveTo: -95, 36
LineTo: -95, 33
MoveTo: -95, 33
LineTo: -99, 33
Drawing code snippet:
//Function used to to get start and stop points
LSNLineObj.GetEndPoints(X1,Y1,X2,Y2);
//OutputMemo.Text := OutputMemo.Text + #13#10 + 'LineStartVal: ' + FloatToStrF(X1, ffGeneral, 8, 4) + ', ' + FloatToStrF(Y1, ffGeneral, 8, 4);
//OutputMemo.Text := OutputMemo.Text + #13#10 + 'LineEndVal: ' + FloatToStrF(X2, ffGeneral, 8, 4) + ', ' + FloatToStrF(Y2, ffGeneral, 8, 4);
X1int := Round(X1); X2int := Round(X2);
Y1int := Round(Y1); Y2int := Round(Y2);
PartImage.Canvas.MoveTo(X1int,X2int);
OutputMemo.Text := OutputMemo.Text + #13#10 + 'MoveTo: ' + IntToStr(X1int) + ', ' + IntToStr(Y1int);
PartImage.Canvas.LineTo(X2int,Y2int);
OutputMemo.Text := OutputMemo.Text + #13#10 + 'LineTo: ' + IntToStr(X2int) + ', ' + IntToStr(Y2int);
You have two coordinate systems: first, you have your 'logical' system with coordinates like -88.988857, 36.265838
. Second, you have the screen. You need to convert between these two. You should write functions
function LogToScreen(LogPoint: TRealVector): TPoint;
function ScreenToLog(Point: TPoint): TRealVector;
where TRealVector
is a record containing two doubles. Writing these two functions requires only elementary-school mathematics.
For instance, you could let the on-screen rectangle 0..800
and 0..600
correspond to logical values -110..-80
and 30..40
.
Hint: With the values as above,
function LogToScreen(LogPoint: TRealVector): TPoint;
begin
result.X := round(800 * (LogPoint.X - (-110)) / ((-80) - (-110)));
result.Y := round(600 * (LogPoint.Y - 30) / (40 - 30)); // or rev. orientation
end;
There are several parts to this question. 1) How to scale World coordinates to Screen coordinates. 2) How to load the shape data, 3) How to plot the data
to the screen.
The data conversion information comes from Buro Tshaggelar's article ( http://www.ibrtses.com/delphi/dmcs.html ). He discusses how to do the data conversions.
The x, y coordinates of the upper left corner of a Tpaintbox or a Timage are 0, 0 . They must be integer values. You can convert from World coordinates to Screen coordinate or from screen coordinates to World coordinates by scaling with respect to an offset value.
You can plot a shape to either the Tpaintbox or TImage Canvas. If you plot to a Tpaintbox, the result is not persistent.
Instead of drawing individual lines to create your shape, I suggest you make use of an array of Tpoint to draw your shape. The following works in XE2 VCL.
Part 1
When the World coordinates are decimal values, the coordinates are bounded by a box with coordinates xLowvalue, xHighValue and yLowVaue and yHighValue, the
screen window can be defined as tlx..brx, tly..bry where the coordinates are integer values.
The following functions convert between World and screen coordinates. Andreas' conversions work too. I like them and they are more efficient in they convert the coordinate pairs simultaneously. I prefer the conversions below to describe the process.
Convert from World coordinates to screen coordinates:
function mapW2SxLin(xf:double):integer;
begin
result:= round(tlx + (xf - xlow) * (brx - tlx) / (xhigh - xlow));
end;
function mapW2SyLin(yf:double):integer;
begin
result:= round(bry - (yf - ylow) * (bry - tly) / (yhigh - ylow));
end;
Convert from Screen coordinates to World coordinates:
function mapS2WxLin(xs:integer):double;
begin
result:= xlow +(xs - tlx) * (xhigh - xlow )/ (brx - tlx);
end;
function mapS2WyLin(ys:integer):double;
begin
result:= yhigh - (ys - tly) * (yhigh - ylow) / (bry - tly);
end;
In the example provided by ikathygreat, the conversion desired appears to be from Cartesian to screen coordinates data (the values provided seem to be latitude and longitude position pairs presented as decimal latitude and longitude values).
Part 2
Instead of loading a data file, the data is hard coded here. Goodle for populate a tpoint array from a file for examples of loading the xy dynamic array. There is code to that will let you load data from a text file. Be aware, when you load the data, you have to provide code to change the size of the array (6) in SetLength (It is currently coded Setlength(xy, 6); ) and sets the size of the array at 6. How to do this should be another question. The value will change depending on the number of vertices in your shape.
Part 3
Plotting the shape using a Tpoint array. What was asked for but not using the requestor's methodlogy. Drawing individual lines is a bit awkward to code for plotting a shape. I believe the example below is a simpler solution and gets the job done.
Set up you paint box with bounds reflecting the maximum limits on the World coordinates using:
xLow := -88; // the easternmost longitude provided
xHigh:= -100; // the westernmost longitude provided
yLow:= 37; // the highest latitude provided
yHigh:= 32; // the lowest latitude provided
I suggest Global variables be defined like:
var
xLow,xHigh,yLow,yHigh:double;
tlx,brx,tly,bry:integer;
xy: array of TPoint;
To plot the shape using an array, you need to define a type
type
TMyPolygon = array of TPoint; // a dynamic array
and assign these values on your OnCreate form event handler:
brx:=Paintbox1.Left;
tlx:=Paintbox1.Left + paintbox1.Width ;
bry:= Paintbox1.Top;
tly :=Paintbox1.Top + paintbox1.Height;
Add an Image, a Paintbox and a Button to a form. Then use the following code in the Button and the Form OnCreate handler. Also add the conversion functions and Global variables (xLow,xHigh,yLow,yHigh:double; tlx,brx,tly,bry:integer; xy: array of Point;) and remember to add the Tpoint type: ( type TMyPolygon = array of TPoint;)
implementation
{$R *.dfm}
function mapW2SxLin(xf:double):integer;
begin
result:=round(tlx+(xf-xlow)*(brx-tlx)/(xhigh-xlow));
end;
function mapW2SyLin(yf:double):integer;
begin
result:=round(bry-(yf-ylow)*(bry-tly)/(yhigh-ylow));
end;
function mapS2WxLin(xs:integer):double;
begin
result:=xlow+(xs-tlx)*(xhigh-xlow)/(brx-tlx);
end;
function mapS2WyLin(ys:integer):double;
begin
result:=yhigh-(ys-tly)*(yhigh-ylow)/(bry-tly);
end;
procedure TPlotShapeFm.Button1Click(Sender: TObject);
var
// xy: array of TPoint; //probably want to define this globally
x,y,x1,y1,x2,y2,x3,y3:integer;
ax,ay,ax2,ay2:integer;
begin
{ Your values
-88.988857, 36.265838
-89.094923, 36.371904
-89.094923, 36.371904
-95.000423, 36.371904
-95.000423, 36.371904
-95.000423, 32.828604
-95.000423, 32.828604
-99.134273, 32.828604
-88.988857, 36.265838 //repeat the first value to close the shape
}
//convert from World to screen coordinates
// these values are hard coded for this example
// there are many ways to load these from a text file
x:= mapW2SxLin(-88.988857);
y:= mapW2SyLin( 36.265838 );
ax:= mapW2SxLin(-89.094923);
ay:= mapW2SyLin(36.371904);
x1:= mapW2SxLin(-95.000423);
y1:= mapW2SyLin(36.371904);
x2:= mapW2SxLin(-95.000423);
y2:= mapW2SyLin(32.828604);
ax2:= mapW2SxLin(-99.134273);
ay2:= mapW2SyLin(32.828604);
x3:= mapW2SxLin(-88.988857); //return to the starting coordinates to finish off the shape
y3:= mapW2SyLin(36.265838 );
// populate the dynamic array
Setlength(xy, 6);
xy[0] := point(x,y);
xy[1] := point(ax,ay);
xy[2] := point(x1,y1);
xy[3] := point(x2,y2);
xy[4] := point(ax2,ay2);
xy[5] := point(x3,y3);
Paintbox1.Canvas.Brush.Color := Random($FFFFFF);
//plot the shape
//canvas.Polygon(xy); //generic or plot on the form itself
// or
Image1.canvas.polygon(xy); //to plot on a Timage
Paintbox1.canvas. polygon(xy); //to plot on a Tpaintbox
end;
procedure TPlotShapeFm.FormCreate(Sender: TObject);
begin
//You can set up you paint box using:
{ xLow := 0;
xHigh:=-180;
yLow:= 50;
yHigh:= 30; //to display part of North America or
xLow := 180;
xHigh:=-180;
yLow:= 90;
yHigh:= 00; //to display the entire World, North of the equator.
}
// but to display the info provided as a large image
xLow := -88;
xHigh:=-100;
yLow:= 37;
yHigh:= 32;
// scale the paintbox to World coordinates
brx:= Paintbox1.Left;
tlx:= Paintbox1.Left + paintbox1.Width;
bry:= Paintbox1.Top;
tly := Paintbox1.Top + paintbox1.Height;
end;
Do it right, the result is like:
As I look at the provided data and the resulting shape, I note the data may not have been listed in the order the requester desires to draw the shape correctly using an array (note the cross-over). The crossover can be fixed by listing the points in clock-wise or counter-clockwise order starting at the initial point, then finishing at the initial point. The shape MUST be closed by finishing the drawing at the initial point.
"How can I draw my shape from a list of decimal X,Y points?"
http://graphics32.org/
var
pts: TArrayOfFixedPoint;
fr: TFloatRect;
begin
...
pts := MakeArrayOfFixedPoints(fr);
SimpleFill(Bitmap, pts, Color32(clBlack), clCornSilk32);
Hunt down the following extras: GR32_Lines, GR32_Misc, GR32_Misc2, GR32_Text. Add the files to the runtime package. The design time package is properly dependent on the r/t package, so no extra steps there. I recently built in XE5 and XE7.