Draw image in canvas with decimal values

2019-08-29 11:04发布

问题:

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);

回答1:

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;


回答2:

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.



回答3:

"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.