获取到经纬度范围后,我们需要计算出瓦片的范围。
本文涉及的地图瓦片都以左上角为原点开始编号的,从左至右为 x 轴, 从上到下为 y轴。
为保证地图是方形,基于 Web 墨卡托投影的地图左上角经纬度坐标为(180°,85.0511 °),右下角经纬度为(-180°,-85.0511°)。纬度范围是[-85.0511, 85.0511 ]。
假设z为需要拼接的图层的层数,设n=2的z次方,lon为经度,lat为维度,则经纬度、层级和瓦片的坐标x、y的关系为:
TileX =(lon+180)÷360×n;
TileY = (1-(log(tan(lat))+sec(lat)))÷2×n;
在C#中,Math函数的三角参数要求为弧度,所以纬度值牵扯到三角函数的还需要×PI÷180。两个函数如下:
/// <summary>
/// 计算瓦片所在X值
/// </summary>
/// <param name="lng"></param>
/// <param name="zoom"></param>
/// <returns></returns>
public static double getTileX(double lng, int zoom)
{
double tileX = (lng + 180.0) / 360 * Math.Pow(2, zoom);
return tileX;
}
/// <summary>
/// 墨卡托投影下,通过维度计算瓦片所在Y值
/// </summary>
/// <param name="lat"></param>
/// <param name="zoom"></param>
/// <returns></returns>
public static double getTileY(double lat, int zoom)
{
if (lat > 90)
lat = lat - 180;
if (lat < -90)
lat = lat + 180;
double tileY = Math.Pow(2, zoom) / 2 * (1 - (Math.Log(Math.Tan(Math.PI * lat / 180) + 1 / Math.Cos(Math.PI * lat / 180))) / Math.PI);
return tileY;
}
前面将下载图片的函数已在第三章中写好,直接通过xyz和瓦片地址下载图片,然后使用GDI+拼接即可。
这里有一个很严重的问题,就是受GDI+技术限制,图幅过大可能拼接失败。一般10000像素×10000像素以内为妥。所以,过大范围的需求,只能拼接为系统承受范围内的大小了。我们做一个参数,可以设置这个值,最大到10240像素。用户最终在ps里再量力而行的拼接了。
写一个类DownloadWork,用于在线程里展开下载任务,在构造函数中声明下载范围和下载图片的参数。
GetArea(WorkArg arg)用于对任务进行拆解,以防止GDI+内存溢出报错:
/// <summary>
/// 以maxnum×maxnum为一组数据
/// </summary>
/// <param name="Arg"></param>
/// <returns></returns>
private List<AreaConfig> GetAreas(WorkArg Arg)
{
List<AreaConfig> results = new List<AreaConfig>();
for (int j = Arg.Starty, n = 0; j <= Arg.Endy; n++)
{
for (int i = Arg.Startx, m = 0; i <= Arg.Endx; m++)
{
AreaConfig area = new AreaConfig();
area.Startx = i;
area.Endx = i + GlobalConfig.maxtilecount < Arg.Endx ? i + GlobalConfig.maxtilecount : Arg.Endx + 1;
area.Starty = j;
area.Endy = j + GlobalConfig.maxtilecount < Arg.Endy ? j + GlobalConfig.maxtilecount : Arg.Endy + 1;
area.Posx = m;
area.Posy = n;
i += GlobalConfig.maxtilecount;
results.Add(area);
}
j += GlobalConfig.maxtilecount;
}
return results;
}
主下载拼接函数主要有以下任务:一是拆分任务,二是根据任务大小,创建一个bitmap,三是循环x和y下载图,并根据坐标绘制在bitmap上,四是保存bitmap到本地磁盘。其中,在三中还完成了复合图的制作,代码如下。
public void DoWork(object arg)
{
WorkArg Arg = (WorkArg)arg;
FileInfo fInfo=new FileInfo(Arg.Filename);
string savedir = "";
int xNum = (Arg.Endx - Arg.Startx + 1), yNum = (Arg.Endy - Arg.Starty + 1);
int totalnum=xNum*yNum,count = 0;
//受GDI的限制,每次拼图不能超过20000像素×20000像素,但实际操作过程中,远不止这个数,有时候10000像素也会出错,为了程序的稳定性,
//如果设置单幅图最大不超过20×20个瓦片,就是5120×5120像素
//此处需要对数据进行拆分
List<AreaConfig> tasks = GetAreas(Arg);
for (int taski = 0; taski < tasks.Count; taski++)
{
int xnum = tasks[taski].Endx - tasks[taski].Startx, ynum = tasks[taski].Endy - tasks[taski].Starty;
Bitmap bit = new Bitmap(xnum * 256, ynum * 256);
Graphics g = Graphics.FromImage(bit);
for (int i = tasks[taski].Startx, m = 0; i < tasks[taski].Endx; i++, m++)
{
for (int j = tasks[taski].Starty, n = 0; j < tasks[taski].Endy; j++, n++)
{
if (!iswork)
break;
foreach (var url in Arg.Downloadurls)
{
string durl = Tools.getUrl(url, i, j, Arg.Zoom);
byte[] bts = Tools.downloadTile(durl);
if (bts == null)
{
g.DrawString("此区域请求失败", new Font("宋体", 12f), new SolidBrush(Color.Red), new Point(m * 256, n * 256));
}
else
{
MemoryStream ms = new MemoryStream(bts);
Image img = Bitmap.FromStream(ms);
g.DrawImage(img, m * 256, n * 256, 256, 256);
}
}
count++;
myWorkPercent(count, totalnum,taski+1,tasks.Count);
}
if (!iswork)
break;
}
string filename = fInfo.DirectoryName + "\\" + fInfo.Name + "\\";
if (!Directory.Exists(filename))
Directory.CreateDirectory(filename);
savedir = filename;
if (tasks.Count > 1)
{
filename += tasks[taski].Posy + "-" + tasks[taski].Posx + "_";
}
filename+=fInfo.Name;
bit.Save(filename, ImageFormat.Jpeg);
g.Dispose();
bit.Dispose();
}
myFinishWork(savedir);
}
最后,启动线程:
public void StartWork()
{
iswork = true;
th = new Thread(new ParameterizedThreadStart(DoWork));
th.Start(this.workarg);
}
增加2个事件代理,分别传回工作状态和合并完成状态:
public delegate void Workpercent(int curnum, int maxnum, int ctasknum, int tasknum);
public delegate void FinishWork(string filedir);
合并完成后,通知主界面打开任务文件夹:
void dlw_myFinishWork(string savedir)
{
MessageBox.Show("任务完成!");
enableControls(true);
System.Diagnostics.Process.Start("explorer.exe", savedir);
dlw = null;
}
至此,所有工作基本完成。最后在啰嗦2点:
1.最好使用天地图,占位高,不足就是每天数量有限制,可以自己申请开发者的tk,改config.txt后下载。
2.国外图只作为参考,边境问题敏感,立场要坚定,使用要谨慎。
链接:https://pan.baidu.com/s/1dGzf3GRY50cqmBL-oA-PGA
提取码:v77a
复制这段内容后打开百度网盘手机App,操作更方便哦
本章参考:
芒果香蕉_《地图瓦片编号与经纬度的换算关系》简书