打造地图拼接利器(五)地图采集与拼接

获取到经纬度范围后,我们需要计算出瓦片的范围。

本文涉及的地图瓦片都以左上角为原点开始编号的,从左至右为 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,操作更方便哦

本章参考:

芒果香蕉_《地图瓦片编号与经纬度的换算关系》简书

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容