1. SharpMap中屏幕坐标和地图Map坐标转换:
1 using System.Drawing; 2 using GeoAPI.Geometries; 3
4 namespace SharpMap.Utilities 5 { 6 /// <summary>
7 /// Class for transforming between world and image coordinate 8 /// </summary>
9 public class Transform 10 { 11 /// <summary>
12 /// Transforms from world coordinate system (WCS) to image coordinates 13 /// 将世界坐标转换为image坐标 14 /// NOTE: This method DOES NOT take the MapTransform property into account (use <see cref="Map.WorldToImage(GeoAPI.Geometries.Coordinate,bool)"/> instead) 15 /// </summary>
16 /// <param name="p">Point in WCS</param>
17 /// <param name="map">Map reference</param>
18 /// <returns>Point in image coordinates</returns>
19 public static PointF WorldtoMap(Coordinate p, Map map) 20 { 21 //if (map.MapTransform != null && !map.MapTransform.IsIdentity) 22 // map.MapTransform.TransformPoints(new System.Drawing.PointF[] { p });
23 if (p.IsEmpty()) 24 return PointF.Empty; 25
26 var result = new PointF(); 27
28 var height = (map.Zoom * map.Size.Height) / map.Size.Width; 29 var left = map.Center.X - map.Zoom * 0.5; 30 var top = map.Center.Y + height * 0.5 * map.PixelAspectRatio; 31 result.X = (float)((p.X - left) / map.PixelWidth); 32 result.Y = (float)((top - p.Y) / map.PixelHeight); 33 if (double.IsNaN(result.X) || double.IsNaN(result.Y)) 34 result = PointF.Empty; 35 return result; 36 } 37
38 /// <summary>
39 /// Transforms from image coordinates to world coordinate system (WCS). 40 /// NOTE: This method DOES NOT take the MapTransform property into account (use <see cref="Map.ImageToWorld(System.Drawing.PointF,bool)"/> instead) 41 /// </summary>
42 /// <param name="p">Point in image coordinate system</param>
43 /// <param name="map">Map reference</param>
44 /// <returns>Point in WCS</returns>
45 public static Coordinate MapToWorld(PointF p, Map map) 46 { 47 if (map.Center.IsEmpty() || double.IsNaN(map.MapHeight)) 48 { 49 return new Coordinate(0, 0); 50 } 51 var ul = new Coordinate(map.Center.X - map.Zoom * .5, map.Center.Y + map.MapHeight * .5); 52 return new Coordinate(ul.X + p.X * map.PixelWidth, 53 ul.Y - p.Y * map.PixelHeight); 54 } 55 } 56 }
详细分析: http://www.cnblogs.com/yhlx125/archive/2012/02/10/2342282.html
2. OpenS-CAD中的实现
已知屏幕分辨率每英寸像素点数,一般为96dpi, 定义float m_screenResolution = 96;
1. 初始化CanvasCtrl时,首先调用OnResize()方法。
1 protected override void OnResize(EventArgs e) 2 { 3 base.OnResize(e); 4 5 if (m_lastCenterPoint != UnitPoint.Empty && Width != 0) 6 SetCenterScreen(ToScreen(m_lastCenterPoint), false); 7 m_lastCenterPoint = CenterPointUnit(); 8 m_staticImage = null; 9 DoInvalidate(true); 10 }
由于m_lastCenterPoint是结构体变量,所以首先设置到中心点m_lastCenterPoint,即(0,0),是Unit坐标
if (m_lastCenterPoint != UnitPoint.Empty && Width != 0)
SetCenterScreen(ToScreen(m_lastCenterPoint), false);
接着调用m_lastCenterPoint = CenterPointUnit();
通过直角坐标的左上角点和右下角点计算。其实初始化时候执行该方法没有起到设置基准点的作用。可以跳过这2次,等窗体Resize的时候再看。
1 public UnitPoint CenterPointUnit() 2 { 3 UnitPoint p1 = ScreenTopLeftToUnitPoint(); 4 UnitPoint p2 = ScreenBottomRightToUnitPoint(); 5 UnitPoint center = new UnitPoint(); 6 center.X = (p1.X + p2.X) / 2; 7 center.Y = (p1.Y + p2.Y) / 2; 8 return center; 9 } 10 public UnitPoint ScreenTopLeftToUnitPoint() 11 { 12 return ToUnit(new PointF(0, 0)); 13 } 14 public UnitPoint ScreenBottomRightToUnitPoint() 15 { 16 return ToUnit(new PointF(this.ClientRectangle.Width, this.ClientRectangle.Height)); 17 }
2. 接着打开文档DocumentForm,在构造的过程中设置文档画布的视点中心坐标为(0,0)。这样就实现了绘图画布原点坐标和屏幕(客户区)中心点的对应,形成基准点。默认的数据的长度单位为inch,屏幕坐标的单位为像素。这两者之间存在比例关系,通过Zoom缩放比例来实现尺度的变换,同时结合平移量和偏移距离计算出鼠标点的世界坐标。
m_canvas.SetCenter(new UnitPoint(0, 0));
或者加载完数据,设置画布的视点中心坐标。
m_canvas.SetCenter(m_data.CenterPoint);
1 public DocumentForm(string filename) 2 { 3 InitializeComponent(); 4
5 Text = "<New Document>"; 6 m_data = new DataModel(); 7 if (filename.Length > 0 && File.Exists(filename) && m_data.Load(filename)) 8 { 9 Text = filename; 10 m_filename = filename; 11 } 12
13 m_canvas = new CanvasCtrl(this, m_data); 14 m_canvas.Dock = DockStyle.Fill; 15 Controls.Add(m_canvas); 16 m_canvas.SetCenter(new UnitPoint(0, 0)); 17 m_canvas.RunningSnaps = new Type[] 18 { 19 typeof(VertextSnapPoint), 20 typeof(MidpointSnapPoint), 21 typeof(IntersectSnapPoint), 22 typeof(QuadrantSnapPoint), 23 typeof(CenterSnapPoint), 24 typeof(DivisionSnapPoint), 25 }; 26
27 m_canvas.AddQuickSnapType(Keys.N, typeof(NearestSnapPoint)); 28 m_canvas.AddQuickSnapType(Keys.M, typeof(MidpointSnapPoint)); 29 m_canvas.AddQuickSnapType(Keys.I, typeof(IntersectSnapPoint)); 30 m_canvas.AddQuickSnapType(Keys.V, typeof(VertextSnapPoint)); 31 m_canvas.AddQuickSnapType(Keys.P, typeof(PerpendicularSnapPoint)); 32 m_canvas.AddQuickSnapType(Keys.Q, typeof(QuadrantSnapPoint)); 33 m_canvas.AddQuickSnapType(Keys.C, typeof(CenterSnapPoint)); 34 m_canvas.AddQuickSnapType(Keys.T, typeof(TangentSnapPoint)); 35 m_canvas.AddQuickSnapType(Keys.D, typeof(DivisionSnapPoint)); 36
37 m_canvas.KeyDown += new KeyEventHandler(OnCanvasKeyDown); 38 SetupMenuItems(); 39 SetupDrawTools(); 40 SetupLayerToolstrip(); 41 SetupEditTools(); 42 UpdateLayerUI(); 43
44 MenuStrip menuitem = new MenuStrip();//创建文档的主菜单
45 menuitem.Items.Add(m_menuItems.GetMenuStrip("edit")); 46 menuitem.Items.Add(m_menuItems.GetMenuStrip("draw")); 47 menuitem.Visible = false; 48 Controls.Add(menuitem); 49 this.MainMenuStrip = menuitem; 50 } 51 protected override void OnLoad(EventArgs e) 52 { 53 base.OnLoad(e); 54 m_canvas.SetCenter(m_data.CenterPoint); 55 }
2.1查看SetCenter(UnitPoint unitPoint)方法,首先调用PointF point = ToScreen(unitPoint);将unitPoint转换PointF point.找到地图原点对应的屏幕坐标,应该是屏幕左下角点偏移(25,-25)。
1 /// <summary> 2 /// 设置画布到屏幕的中心 3 /// </summary> 4 /// <param name="rPoint">直角坐标系坐标</param> 5 public void SetCenter(RPoint unitPoint) 6 { 7 //将unitPoint点对应到屏幕上point 8 PointF point = Transform.ToScreen(unitPoint, this); 9 m_lastCenterPoint = unitPoint; 10 //将unitPoint偏移到屏幕中心 11 SetCenterScreen(point, false); 12 }
这里注意计算Unit坐标到屏幕坐标的ToScreen()方法中,transformedPoint.Y = ScreenHeight() - transformedPoint.Y;//将Unit坐标系转换为屏幕坐标系,Y轴反向
其中ScreenHeight()方法似乎有点问题,修改后如下。
1 /// <summary> 2 /// 将Unit坐标转换到屏幕坐标 3 /// </summary> 4 /// <param name="point"></param> 5 /// <returns></returns> 6 public PointF ToScreen(UnitPoint point) 7 { 8 PointF transformedPoint = Translate(point); 9 transformedPoint.Y = ScreenHeight() - transformedPoint.Y;//将Unit坐标系转换为屏幕坐标系,Y轴反向 10 transformedPoint.Y *= m_screenResolution * m_model.Zoom; 11 transformedPoint.X *= m_screenResolution * m_model.Zoom; 12 13 transformedPoint.X += m_panOffset.X + m_dragOffset.X; 14 transformedPoint.Y += m_panOffset.Y + m_dragOffset.Y; 15 return transformedPoint; 16 }
1 float ScreenHeight() 2 { 3 return (float)(ToUnit(this.ClientRectangle.Height)); 4 //return (float)(ToUnit(this.ClientRectangle.Height) / m_model.Zoom); 5 }
这样就引出了ToUnit(float screenvalue)函数,将屏幕距离转换为英寸数。
1 public double ToUnit(float screenvalue) 2 { 3 return (double)screenvalue / (double)(m_screenResolution * m_model.Zoom); 4 }
3. 接着将unitPoint赋值给m_lastCenterPoint
m_lastCenterPoint = unitPoint;
SetCenterScreen(point, false);
调用了SetCenterScreen()方法,
1 protected void SetCenterScreen(PointF screenPoint, bool setCursor) 2 { 3 float centerX = ClientRectangle.Width / 2; 4 m_panOffset.X += centerX - screenPoint.X; 5
6 float centerY = ClientRectangle.Height / 2; 7 m_panOffset.Y += centerY - screenPoint.Y; 8
9 if (setCursor) 10 Cursor.Position = this.PointToScreen(new Point((int)centerX, (int)centerY)); 11 DoInvalidate(true); 12 }
4.理解了public PointF ToScreen(UnitPoint point),那public UnitPoint ToUnit(PointF screenpoint)也好理解了。
1 /// <summary> 2 /// 将屏幕坐标转换到Unit坐标 3 /// </summary> 4 /// <param name="screenpoint"></param> 5 /// <returns></returns> 6 public UnitPoint ToUnit(PointF screenpoint) 7 { 8 float panoffsetX = m_panOffset.X + m_dragOffset.X; 9 float panoffsetY = m_panOffset.Y + m_dragOffset.Y; 10 float xpos = (screenpoint.X - panoffsetX) / (m_screenResolution * m_model.Zoom); 11 float ypos = ScreenHeight() - ((screenpoint.Y - panoffsetY)) / (m_screenResolution * m_model.Zoom); 12 return new UnitPoint(xpos, ypos); 13 }
5.最后单独说一下ToScreen(UnitPoint point)和ToUnit(PointF screenpoint)中的两个变量
PointF m_panOffset = new PointF(25, -25); PointF m_dragOffset = new PointF(0, 0);
这里m_panOffset控制的是中心点Center的偏移量,是一个累计的量,相对于中心点。
m_dragOffset记录了每次移动过程中的移动量,每次产生一个新值。每次CanvasCtrl控件的OnMouseDown时累积到偏移量上,之后重新初始化,同时在OnMouseUp时的移动命令下重新初始化(如下代码),似乎重复了。
1 if (m_commandType == eCommandType.pan) 2 { 3 m_panOffset.X += m_dragOffset.X; 4 m_panOffset.Y += m_dragOffset.Y; 5 m_dragOffset = new PointF(0, 0); 6 }
在protected override void OnMouseMove(MouseEventArgs e)事件中
1 if (m_commandType == eCommandType.pan && e.Button == MouseButtons.Left) 2 { 3 m_dragOffset.X = -(m_mousedownPoint.X - e.X); 4 m_dragOffset.Y = -(m_mousedownPoint.Y - e.Y); 5 m_lastCenterPoint = CenterPointUnit(); 6 DoInvalidate(true); 7 }
可知,m_dragOffset和m_panOffset记录的是偏移的屏幕坐标。