手写地理信息组件系列 第3篇
Map坐标变换的实现原理
难度指数:★★☆☆☆
目录:
前情回顾
这一系列文章都是以由简入深的方式展开的,上一章对GIS当中基本的Geometry对象进行了一次更为清晰的重构,梳理了各对象间的继承组合关系。结构更体系了,调用也更方便。而这一篇将在之前的基础上扩展,进一步讨论关于空间对象的显示。由此你将会明确,空间对象显示的屏幕坐标与地图坐标之间的转换,有哪些更为细节的问题。并着手实现一个可以地理坐标系显示的地图程序,一起动手吧。
屏幕坐标与地图坐标
我们面对的屏幕,不管是PC屏幕还是手机屏幕,本质是一系列像素点构成的二维矩阵,通常为矩形。现在显示设备的像素越来越高,几年前PC普遍是1366x768的,现在不到1920x1080像素的屏幕根本没人去买,而手机屏幕好多都高过1920x1080了,已经到了察觉不到像素点的程度,显示非常细腻。
这里说的像素点,其在整个二维矩阵中的位置就是像素坐标。例如在一个1920x1080的屏幕上,位于屏幕区最左上角的像素点坐标规定为(0,0),相应地,右下角的像素点位置为(1919,1079)。
地图坐标可以表示地理空间的某个位置,常见用经纬度这种地理坐标来表示,同时也可以用投影坐标来表示,投影坐标由地理坐标投影后得来,一般单位为米。涉及到地理坐标和投影变换的知识,将在以后专门介绍。此篇中的地图坐标可视为投影坐标。
地图类的构造
地图(Map)可以理解为观察世界的一个窗口,这个窗口内的世界范围是可变的。通过固定地图窗口的大小,调节地理范围,形成地图显示效果的变化,也就是常说的地图缩放(zoom)。
实现地图的缩放,需要计算比例尺(scale),形成地理坐标和像素坐标的对应关系。继而进行两种坐标之间的转换。比例尺的数值由两坐标系各自形成的距离比值得来。
下面来实现屏幕坐标和地图坐标的转换。
Map类是地图显示中最常用的类,后续更高级的显示功能都将在此基础上展开。
public class Map
{
//地图范围
Extent mapExtent;
//地图窗口范围
Rectangle rectangle;
//横纵轴比例尺(实际Size/窗口Size)
double scaleX, scaleY;
public Map()
{
//无参构造函数,仅做内部变量初始化
Update(new Extent(new Vertex(300, 0), new Vertex(0, 300)),//左下、右上组成一个范围
new Rectangle(0, 0, 100, 100));
}
/// <summary>
///
/// </summary>
/// <param name="mapExtent">地图范围</param>
/// <param name="rectangle">地图窗口范围</param>
public void Update(Extent mapExtent, Rectangle rectangle)
{
this.mapExtent = mapExtent;
this.rectangle = rectangle;
scaleX = this.mapExtent.Width / this.rectangle.Width;
scaleY = this.mapExtent.Height / this.rectangle.Height;
}
public System.Drawing.Point ToScreenPoint(Vertex vertex)
{
double x = (vertex.x - mapExtent.MinX) / scaleX;
//屏幕坐标Y轴向下,地图坐标Y轴向上,请读者体会这里的算法
double y = this.rectangle.Height - (vertex.y - mapExtent.MinY) / scaleY;
return new System.Drawing.Point((int)x, (int)y);
}
public Vertex ToMapVertex(System.Drawing.Point point)
{
double x = point.X * scaleX;
double y = point.Y * scaleY;
return new Vertex(x, y);
}
}
public class Extent
{
//左下
Vertex bottomLeft;
//右上
Vertex upRight;
public double MinX
{
get { return bottomLeft.x; }
}
public double MinY
{
get { return bottomLeft.y; }
}
public double MaxX
{
get { return upRight.x; }
}
public double MaxY
{
get { return upRight.y; }
}
public double Width
{
get { return upRight.x - bottomLeft.x; }
}
public double Height
{
get { return upRight.y - bottomLeft.y; }
}
public Extent(Vertex bottomleft, Vertex upright)
{
this.bottomLeft = bottomleft;
this.upRight = upright;
}
}
涉及Geometry对象的重构
在之前的Geometry系列对象中,坐标都是以屏幕坐标作为表示的,其绘制也直接以屏幕坐标绘制。引入地图坐标概念后,坐标必须需要经过转换后才能正确绘制,需要对以下对象进行改动。
class Point : Geometry
{
public Point(Vertex vertex)
{
centroid = vertex;
extent = new Extent(vertex, vertex);
}
public double Distance(Vertex another)
{
return centroid.Distance(another);
}
//新增map参数
public override void Draw(Graphics graphics, Map map)
{
//增加地图坐标到屏幕坐标的转换
System.Drawing.Point point = map.ToScreenPoint(centroid);
graphics.FillEllipse(new SolidBrush(Color.Red),
new Rectangle(point.X, point.Y, 5, 5));
}
}
public abstract class Geometry
{
//质心点
protected Vertex centroid;
//外接矩形
protected Extent extent;
//为了类的安全性,访问器只设置get,一经初始化,不可由非子类更改
public Vertex Centroid
{
get { return centroid; }
}
public Extent Extent
{
get { return extent; }
}
public abstract void Draw(Graphics graphics, Map map);
}
对属性绘制方法的改动:
class Attribute
{
//C#中用以存储键值对的一种容器类,这里用来存储字段名和字段值
private Hashtable table = new Hashtable();
public void AddValue(string fieldName, Object value)
{
table.Add(fieldName, value);
}
public Object GetValue(string fieldName)
{
return table[fieldName];
}
//界面绘制字段值
public void Draw(Graphics graphics, Vertex location, string key)
{
graphics.DrawString(table[key].ToString(), new Font("宋体", 20),
new SolidBrush(Color.Blue), new PointF((int)location.x, (int)location.y));
}
//增加map参数及坐标转换
public void Draw(Graphics graphics, Map map, Vertex location, string key)
{
System.Drawing.Point point = map.ToScreenPoint(location);
//graphics.DrawString
string name = table[key].ToString();
graphics.DrawString(name,
new Font("宋体", 20),
new SolidBrush(Color.Blue),
new PointF(point.X, point.Y));
}
}
对要素绘制方法的改动:
class Feature
{
private Geometry geometry;
private Attribute attribute;
public Feature(Geometry geometry, Attribute attribute)
{
this.geometry = geometry;
this.attribute = attribute;
}
public void Draw(Graphics graphics, Map map, string fieldName)
{
geometry.Draw(graphics, map);
attribute.Draw(graphics, map, geometry.Centroid, fieldName);
}
public Geometry GetGeometry()
{
return geometry;
}
public Object GetAttributeValue(String fieldName)
{
return attribute.GetValue(fieldName);
}
}
通过对以上对象的观察,可以发现主要是增加了坐标转换的步骤,未对各对象的实质功能做出改变。这里省却了各对象未修改的部分,具体可以参考上一篇,查看完整定义。
地理坐标实体的绘制
设计界面:
界面增加了地图坐标的对象绘制。“添加”分组框的XY为地理实体点的地理坐标输入框,“地理范围”分组框的四个参数,定义了当前地图窗口的地理范围,至于地图窗口的Size,取界面的Rectangle作其范围。
public partial class FormThirdPart : Form
{
Map map;
Extent mapExtent;
List<Feature> features = new List<Feature>();
public FormThirdPart()
{
InitializeComponent();
map = new Map();
btn_MapUpdate_Click(null, null);
}
private void BtnAddPoint_Click(object sender, EventArgs e)
{
double x = Convert.ToDouble(textBox_X.Text);
double y = Convert.ToDouble(textBox_Y.Text);
//构造图形
Vertex vertex = new Vertex(x, y);
GisClass.Point p = new GisClass.Point(vertex);
//构造属性
GisClass.Attribute attr = new GisClass.Attribute();
attr.AddValue(textBox_Attr_Name.Text, textBox_Attr_Value.Text);
//构造要素
Feature feature = new Feature(p, attr);
feature.Draw(this.CreateGraphics(), map, textBox_Attr_Name.Text);
features.Add(feature);
}
private void btn_MapUpdate_Click(object sender, EventArgs e)
{
double minX = Double.Parse(txtBox_minX.Text);
double minY = Double.Parse(txtBox_minY.Text);
double maxX = Double.Parse(txtBox_maxX.Text);
double maxY = Double.Parse(txtBox_maxY.Text);
mapExtent = new Extent(
new Vertex(minX, minY), new Vertex(maxX, maxY));
//重绘
mapUpdate();
}
//距离容限值 10 像素
const int Tolerance = 10;
private void FormThirdPart_MouseClick(object sender, MouseEventArgs e)
{
{
Vertex vertex = new Vertex(e.X, e.Y);
double minDistance = Double.MaxValue;
Feature nearest = null;
//筛选与鼠标点选位置最近的坐标点
foreach (Feature f in features)
{
double distance =
f.GetGeometry().Centroid.Distance(vertex);
if (distance < minDistance)
{
nearest = f;
minDistance = distance;
}
}
if (nearest != null && nearest.GetGeometry().Centroid.Distance(vertex) < Tolerance)
{
MessageBox.Show(
nearest.GetAttributeValue(textBox_DisplayField.Text).ToString());
}
else
{
textBox_X.Text = e.X.ToString();
textBox_Y.Text = e.Y.ToString();
}
}
}
//更新界面--重绘
private void mapUpdate()
{
map.Update(mapExtent, this.ClientRectangle);
Graphics graphic = this.CreateGraphics();
//清理界面,准备重绘
graphic.Clear(this.BackColor);
foreach (Feature f in features)
{
f.Draw(graphic, map, textBox_Attr_Name.Text);
}
graphic.Dispose();
}
private void FormThirdPart_Paint(object sender, PaintEventArgs e)
{
mapUpdate();
}
}
点绘制验证
此之前,窗口内可显示的点坐标XY值是不会超出窗口的长宽范围的,现准备的点坐标为(2000,2000),显然连显示器的显示范围都已经超出了,但是通过设置一个更大的地理范围(0,0)(4000,4000),该点仍能显示在地图窗口之中。
如果对单个点绘制效果感觉不强,做一次多点的测试可能会更加直观。
点1. (100,100)
点2. (100,200)
点3. (300,300)
在(0,0)(600,600)范围下的显示
在(0,0)(1000,1000)范围下的显示
地图缩放的背后原理就是这样,不要求特定的距离单位都能直接绘制。这样在以后涉及到图层的概念时,如果两个图层的坐标系统一致,你会发现可以很轻易的将两个图层叠加在一起,不会出现莫名的错位问题。
此篇就是这些,还有很多更高级的功能及原理正在陆续赶来,看好关注,下期见!