跨平台 C++ 模板解决方案,用于为第三方 GIS 绘制 GPS 数据






4.11/5 (5投票s)
生成 PNG 文件,
介绍
假设您是一名GIS新手,被经理粗略地安排实施一个据称能为某个第三方GIS工作的项目。技术任务相当抽象,让您感到一阵恶心,因为您不熟悉其中使用的术语。
您的技术助理说,您会收到包含您城市交通拥堵GPS坐标的XML数据。您需要创建一个能够生成PNG文件的东西。这些文件称为瓦片,每个大小为256x256像素。文件会被生成到目录中。每个目录都以缩放系数(从8到17)命名。其背后思想是将GPS坐标(纬度和经度)映射到笛卡尔平面(X/Y),根据缩放系数计算像素大小,并通过一个或多个PNG文件(瓦片)绘制折线。最终结果如图1和图2所示。

图1
墨卡托投影 简明扼要
GPS使用地理坐标系。简而言之,纬度是某个点到赤道的角度,经度是极点到极线的角度。我们必须将这些角度投影(转换)到笛卡尔平面(X/Y)。但从表示球面点角度进行投影意味着会产生失真。人类花费了许多年通过采用各种地球投影到平面上的方法来减小这种失真。随着墨卡托投影的出现,革命发生了。从那时起,水手们就可以使用指南针和地图来绘制航线。
此模板项目使用正弦(又称椭圆)墨卡托投影,根据该投影,地球不是一个球体,而是一个在两极扁平的扁球体。可以想象,该投影如同球体内部,而原始墨卡托投影如同圆柱体内部,牺牲了两极。
WGS84 基准
当我们谈论使用投影时,我们谈论的是公式。几乎任何公式都需要常数。就地图投影而言,这些常数是在称为基准的标准中指定的。如今大多数公式都使用WGS84。根据此基准,地球是一个扁球体,其长半轴(又称大半径)长度为6,378,137米。
使用大半径,我们可以根据缩放系数找到像素大小。这通过这个绝妙的公式(进一步的评论请参考数学家)2pr/n*2z
来实现,其中r
是大半径,n
是PNG文件中的像素数,z
是缩放系数。
分块
高峰时段城市被交通拥堵覆盖,所以交通拥堵形成了其大致地图。整个城市几乎可以容纳在一个瓦片中(如图1所示的最小缩放系数8)。我们像飞机一样高高飞翔,甚至更高。一个像素大约611米。在最大缩放系数(17)时,我们下降到离地面只有几米远的高度。一个像素几乎是一米。虽然整个城市在缩放系数8时可以容纳在一个瓦片中,但在缩放系数17时,它被分割成成千上万个瓦片。结果可以想象成图2所示的网格。
我在寻找瓦片边界上的交点上花费了太多时间。无论是由于图形引擎的特性还是计算的不准确性,一个瓦片中的一条线与其在另一个瓦片中的延续都会轻微不匹配。然后我发现图形引擎能够进行裁剪。这意味着我不再需要计算交点(我喜欢称它们为边界点)。图形库会替我完成。我所要做的就是计算某个点相对于某个瓦片*看起来*的像素坐标。我不再需要担心该点是否在瓦片内部或外部,以及是否需要创建交点(如果它在外部)。
![]() |
![]() |
![]() |
图2
编译和执行
本文提出的项目是一个Visual Studio 2008解决方案。它使用GNU库,如表1所示,用于XML解析和绘图,如表1所示。欢迎非Windows用户创建Makefile并发送给我。 :) 如果在调试模式下运行,文件将生成在solution文件夹内的Files目录中。在PNG文件旁边,您会找到JSON(扩展名为.js)文件。通过这些文件,第三方GIS可以在鼠标悬停在特定交通拥堵折线上时显示弹出窗口。您可以根据您的GIS要求重新格式化它们,或通过在stdafx.h中定义_NOT_GENERATE_JSON
宏来关闭它们。输入XML文件jams.xml位于解决方案目录中。默认情况下,它在调试模式下使用。您可以在上面的图片中看到处理此文件的结果。
库名称 | 目的 |
LibXml2 | XML解析器 |
Cairo | 图形引擎 |
LibPng | Cairo用于PNG创建 |
Iconv | 用于各种编码转换 |
Zlib1 | 所有上述库都使用它 |
仅限Windows用户,要编译此解决方案,请下载gnu_libs.zip并将其内容复制到C:\Program Files。
注意!要在调试模式下运行,请将GPRender_exec.zip中的所有DLL复制到可执行文件旁边的..\GPRender\GPRender\Debug目录。
使用代码
图3显示了项目中使用的基本类的UML。不同类型的GIS使用不同的投影算法。我不能确定该项目是否与Google Maps兼容。我唯一知道的是,其中使用的公式曾经与OpenStreetMap兼容。
您可以自由尝试引入其他算法,方法是在GZoomT
类的模板参数中指定您的类。
GZoom.h中的GZoom
类是一个typedef
typedef GZoomT<> GZoom;
GZoomT
将GMercator.h中的GMCalculus
作为其默认模板参数。
template<class T=GMCalculus>
class GZoomT;
GShapes.h中的GPoint
类也是如此。以下是项目基本类的UML图

图3
有趣的 कोड摘录
实际处理在GZoom::Tilerize()
中完成
void inline Tilerize()
{
//tilerizes the jams
groupedbystreet::const_iterator it = mJams.begin();
for ( ; it != mJams.end(); ++it )
{
FillTilesInFirstJam(it->second);
++it;
break;
}
for ( ; it != mJams.end(); ++it )
FillTilesInJam(it->second);
//creates missing tiles and adds points relative to
//current tile or next tile
createMissingTiles();
//merges the tiles with the same name
MergeTheProcessed();
//generates files
for ( set::iterator it = mMergedTiles.begin();
it != mMergedTiles.end(); ++it )
gDevice.DrawPolies( *it );
}
GZoom::FillTilesInJam()
遍历交通拥堵,为每个交通拥堵成员的点列表创建瓦片。在此阶段,每个GTile
都有一个GPolyline
成员。我们不关心瓦片名称的重复。
void inline SplitToTilesWithDups(const GJam& jam, const GPoint& p)
{
GTile& tile = mTiles.back(); //current tile
if ( tile.Contains(p) && tile.GetJamID() == jam.mIDJam )
tile.AddPointCalcPixel(p);
else //ANOTHER TILE - one tile one polyline at this step!!!
mTiles.push_back( CreateNewTile(jam, p) );
CreateArrow(mTiles.back(),jam,p);
}
现在生成任何缺失的瓦片。GZoom::ProcessPair()
填充属于同一交通拥堵的两个相邻瓦片之间的空白。
void inline createMissingTiles()
{
//more code here
if ( currTile.GetJamID() == nextTile.GetJamID() )
{
assert ( 1 == currTile.Polylines().size()
&& 1 == nextTile.Polylines().size() );
ProcessPair(currTile, nextTile);
}
mProcessedTiles.push_back(currTile);
mProcessedTiles.push_back(nextTile);
currTile = *it;
//more code here
}
在此阶段,存在大量重复的瓦片。即名称相同但来自不同交通拥堵的瓦片。因此,我们通过将重复瓦片的GPolyline
元素推送到同名GTile
中来合并它们,并消除重复。这是通过std::set
实现的。
void inline MergeTheProcessed()
{
for ( tilelist::iterator it = mProcessedTiles.begin();
it != mProcessedTiles.end(); ++it )
{
pair<set><gtile>::iterator, bool> p = mMergedTiles.insert(*it);
if ( ! p.second )
{
GTile& prevTile = *(p.first);
GTile& nextTile = *it;
const_cast<polylist&> ( prevTile.Polylines() ).push_back
( nextTile.Polylines().back() );
}
}
}
最后但同样重要的是,在GZoom::Tilerize()
中使用gDevice.DrawPolies()
绘制来自set容器的瓦片。
历史
- 2008年12月21日:首次发布