标签:
上一篇文章讲解如何加载各地图的WMS地图服务。虽然不涉及到瓦片,但是每次地图刷新都要请求网络,造成不小的网络负载。虽然判断视野是否改变确定是否请求网络来减小网络负载,但是这个方法仍然不理想。
谷歌的地图底图自带高程视觉,公路分级样式、行政区域分级样式、地图数据即时的更新速度等等优点,让人觉得有必要开发一个地图下载器。虽然谷歌本身被墙,但是谷歌地图还是可以访问的。地址如下:
http://www.google.cn/maps(可以手动输入:http://maps.google.cn)
谷歌已经关闭了开发者API,现在只能自己动手做一个了。下面仍然新建一个WinForm程序,增加对DotSpatial的引用,加入DotSpatial控件,代码如下:
using DotSpatial.Controls; using DotSpatial.Data; using DotSpatial.Projections; using DotSpatial.Topology; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace GoogleWmts { public partial class MainForm : Form { private Map mapCtrl; private ProjectionInfo currentProjection; public const double WUHAN_WGS84_COORDINATE_Y = 30.883124; public const double WUHAN_WGS84_COORDINATE_X = 114.419915; private Coordinate wuhanCoordinate; public Size lastSize; public MainForm() { mapCtrl = new Map() { Left = 0, Top = 0, Size = new Size(0, 0), Dock = DockStyle.Fill, FunctionMode = FunctionMode.Pan }; InitProjection(); InitializeComponent(); Controls.Add(mapCtrl); } private void InitProjection() { currentProjection = ProjectionInfo.FromEpsgCode(2432); var xy = new double[2] { WUHAN_WGS84_COORDINATE_X, WUHAN_WGS84_COORDINATE_Y }; var z = new double[1]; Reproject.ReprojectPoints(xy, z, KnownCoordinateSystems.Geographic.World.WGS1984, currentProjection, 0, 1); wuhanCoordinate = new Coordinate(xy); } } }很遗憾的是DotSpatial内建的坐标系统并不支持Google的900913坐标系,这里我使用Beijing1954坐标系,中央经线是105度,EPSG的CRSID是2432,预定义武汉的经纬度,用于设定地图初始化视野。currentProjection定义当前地图控件使用的坐标系。天地图、OSM地图、腾讯地图、谷歌的地图都是基于分辨率设定视野,因此新建一个ResolutionLayer类型的图层,以便通用,代码如下:
using DotSpatial.Controls; using DotSpatial.Data; using DotSpatial.Projections; using DotSpatial.Symbology; using DotSpatial.Topology; using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace GoogleWmts { class ResolutionLayer : Layer, IMapLayer { public const double GOOGLE_ORIGIN_X = -20037508.3427892; public const double GOOGLE_ORIGIN_Y = 20037508.3427892; public const int TILE_WIDTH = 256; public const int TILE_HEIGHT = 256; public const int SCREEN_MILLIMETER_WIDTH = 564; public const int SCREEN_PIXEL_WIDTH = 1600; public const int MAX_ZOOM_LEVEL = 18; private Dictionary<int, double> resolutions; string urlFormat = "http://mt2.google.cn/vt/lyrs=m@167000000&hl=zh-CN&gl=cn&x={0}&y={1}&z={2}&s=Galil";//X瓦图号,Y瓦片号,比例尺缩放层级 private Extent defaultExtent; public int ZoomLevel { get; set; } public Size WindowSize { get; set; } public bool WindowCreated { get; set; } public bool IsBusy { get; set; } public ResolutionLayer() { defaultExtent = new Extent(-2000000000, -2000000000, 2000000000, 2000000000); InitDirectory(); SetResolutionsByMath(); } private void InitDirectory() { var rpt = @"Tiles\Google\"; if (!Directory.Exists(rpt)) Directory.CreateDirectory(rpt); for (var i = 0; i <= MAX_ZOOM_LEVEL; i++) { var rp = rpt + @"\" + i; if (!Directory.Exists(rp)) Directory.CreateDirectory(rp); } } public override Extent Extent { get { return defaultExtent; } } } }GOOGLE_ORIGIN_X与GOOGLE_ORIGIN_Y两个常量记录谷歌地图的坐标原点,TILE_WIDTH与TILE_HEIGHT两个常量记录单个瓦片图文件的像素大小。SCREEN_MILLIMETER_WIDTH常量记录当前显示屏幕的物理大小。我当前的显示屏是19吋,物理大小是564毫米,请朋友在使用之前务必改成您自己的显示屏幕的物理大小。SCREEN_PIXEL_WIDTH常量记录的是当前显示屏的素大小。如果您不知道您当前显示设备的像素大小,请查看显示屏属性,找到当前设置的分辨率。我当前的显示屏幕是1600像素的宽度,请朋友在使用之前务必改成你自己的屏幕的分辨率宽度。这里提供一个简单的方法获取屏幕物理大小与像素大小,代码如下:
[DllImport("gdi32.dll", EntryPoint = "GetDeviceCaps", CallingConvention = CallingConvention.Winapi)] public static extern int GetDeviceCaps(IntPtr hdc, int code);
public const int HORZSIZE = 4; var g = CreateGraphics(); var millimeterLength = NativeAPI.GetDeviceCaps(g.GetHdc(), NativeAPI.HORZSIZE); var pixelLength = Screen.PrimaryScreen.Bounds.Width; g.Dispose();
MAX_ZOOM_LEVEL是最大缩放级别,也就是街道级别。resolutions是各层级比例尺下的分辨率。defaultExtent是给DotSpatial计算图层最大视野用的。此变量必须给,否则看不到地图。这与DotSpatial计算视野,确定窗口更新区域的算法有关系。放在这里吧。ZoomLevel 是当前缩放级别,WindowSize记录窗体的实际大小,WindowCreated指示窗口是否已经创建成功。IsBusy指示图层当前是否正在下载瓦片图。如果正在下载中,那么不响应用户放大、缩小、移动等地图操作。InitDirectory方法设定瓦片的存储路径,组织方式是在当前软件的文件夹下新建一个Tiles文件夹,再新建一个Google文件夹,然然针对每一个比例尺新建文件夹,瓦片图文件名称以瓦片索引命名。
谷歌地图的分辨率可能通过计算的方法获取,代码如下:
private void SetResolutionsByMath() { resolutions = new Dictionary<int, double>(); for (var i = 1; i <= MAX_ZOOM_LEVEL; i++) resolutions.Add(i, 20037508.3427892 * 2 / 256 / Math.Pow(2, i)); }
上面说过,DotSpatial不支持Google的900913坐标系,那么必须进行坐标转换。在这里我使用Proj.4 C++库,并封装一个Win32动态库给C#调用,C++的调用Proj.4的代码如下:
typedef __declspec(dllexport) struct _COORDINATE { double x; double y; double z; double m; int srid; }COORDINATE, *PCOORDINATE;
BRIDGE_API BOOL proj4_transform(PCSTR proj4_from, PCSTR proj4_to, COORDINATE* coordinate) { if (proj4_from == nullptr || strlen(proj4_from) < 5) return FALSE; if (proj4_to == nullptr || strlen(proj4_to) < 5) return FALSE; projPJ from = pj_init_plus(proj4_from); projPJ to = pj_init_plus(proj4_to); if (from == nullptr || to == nullptr) return FALSE; int code = pj_transform(from, to, 1, 1, &coordinate->x, &coordinate->y, &coordinate->z); return !code; }C#调用代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace GK.Collector.Server.Entity.DllImports { [StructLayout(LayoutKind.Explicit)] public struct COORDINATE { [FieldOffset(0)] public double x; [FieldOffset(8)] public double y; [FieldOffset(16)] public double z; [FieldOffset(24)] public double m; [FieldOffset(32)] int srid; } }
[DllImport("bridge.dll", EntryPoint = "proj4_transform", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern bool Proj4Transform(string proj4From, string projTo, IntPtr coordinate); public static bool Transform(double[] xyz, string proj4From, string proj4To = "") { if (xyz.Length < 3) return false; if (string.IsNullOrWhiteSpace(proj4From)) return false; if (string.IsNullOrWhiteSpace(proj4To)) proj4To = " +proj=longlat +datum=WGS84 +no_defs"; var c = new COORDINATE() { x = xyz[0], y = xyz[1], z = xyz[2] }; var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(c)); Marshal.StructureToPtr(c, ptr, true); Proj4Transform(proj4From, proj4To, ptr); c = (COORDINATE)Marshal.PtrToStructure(ptr, typeof(COORDINATE)); Marshal.FreeHGlobal(ptr); xyz[0] = c.x; xyz[1] = c.y; xyz[2] = c.z; return true; }把常用的坐标系设定为字符串常量,以方便使用,代码如下:
public const string BJ2432_PROJ = "+proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3,0,0,0,0 +units=m +no_defs "; public const string WORLD3857_PROJ = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"; public const string GOOGLE_PROJ = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs "; public const string WGS84_PROJ = "+proj=longlat +datum=WGS84 +no_defs ";
瓦片索引的计算是重中之重。其中包括瓦片对齐到用户窗口,避免地图移位、拖动不顺畅的问题,代码如下:
/// <summary> /// 获取瓦片图索引以及偏移 /// </summary> /// <param name="cxy">原始坐标</param> /// <param name="txy">瓦片图索引</param> /// <param name="rxy">偏移</param> private void GetTileIndexByCoordinate(double[] cxy, int[] txy, double[] rxy) { var z = new double[1]; var coef = resolutions[ZoomLevel] * TILE_WIDTH; var wgs84Proj = KnownCoordinateSystems.Geographic.World.WGS1984; //DotSpatial.Projections.Reproject.ReprojectPoints(cxy, z, Projection, wgs84Proj, 0, 1); //cxy[0] = cxy[0] * 20037508.3427892 / 180; //cxy[1] = Math.Log(Math.Tan((90 + cxy[1]) * Math.PI / 360)) / (Math.PI / 180); //cxy[1] = cxy[1] * 20037508.3427892 / 180; Transform(cxy, BJ2432_PROJ, GOOGLE_PROJ); txy[0] = (int)((cxy[0] - GOOGLE_ORIGIN_X) / coef); txy[1] = (int)((GOOGLE_ORIGIN_Y - cxy[1]) / coef); rxy[0] = (cxy[0] - GOOGLE_ORIGIN_X) / coef - txy[0]; rxy[1] = (GOOGLE_ORIGIN_Y - cxy[1]) / coef - txy[1]; }得到瓦片索引就可以下载了。用WebClient直接下载发现被谷歌屏蔽,通过Fiddler抓包工具发现可以顺利通过谷歌验证的HTTP包,代码如下:
private Image GetImageByWebClient(double tilex, double tiley) { var rp = @"Tiles\Google\" + ZoomLevel + @"\" + tilex + "_" + tiley + ".png"; if (File.Exists(rp)) { var tb = Image.FromFile(rp); //Console.WriteLine(rp); return tb; } else { string url = string.Format(urlFormat, tilex, tiley, ZoomLevel); //Console.WriteLine(url); var downloader = new WebClient(); downloader.Headers.Add("Upgrade-Insecure-Requests: 1"); downloader.Headers.Add("User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36"); downloader.Headers.Add("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); downloader.Headers.Add("Accept-Encoding: gzip, deflate, sdch"); downloader.Headers.Add("Accept-Language: zh-CN,zh;q=0.8"); try { var bts = downloader.DownloadData(url); var str = new MemoryStream(bts); var img = Image.FromStream(str); img.Save(rp); str.Close(); str.Close(); return img; } catch (Exception e) { Console.WriteLine(e.Message); } downloader.Dispose(); } return null; }
public void DrawRegions(MapArgs args, List<DotSpatial.Data.Extent> regions) { if (!WindowCreated || WindowSize.Width <= 160 || WindowSize.Height <= 30) return; IsBusy = true; var img = new Bitmap(args.ImageRectangle.Width, args.ImageRectangle.Height); var g = Graphics.FromImage(img); var resolution = resolutions[ZoomLevel]; var toffset = new double[2]; foreach (var region in regions) { var leftxy = new double[3] { region.MinX, region.MinY, 0 }; var txy = new int[2]; var rxy = new double[2]; toffset[0] = -1; toffset[1] = -1; GetTileIndexByCoordinate(leftxy, txy, rxy); for (var i = region.MinX; i < region.MaxX; i += resolution * TILE_WIDTH) { for (var j = region.MinY; j < region.MaxY ; j += resolution * TILE_HEIGHT) { var tb = GetImageByWebClient(txy[0] + toffset[0], txy[1] + toffset[1]); var tx = Convert.ToInt32((toffset[0] - rxy[0]) * TILE_WIDTH); var ty = Convert.ToInt32((toffset[1] - rxy[1]) * TILE_HEIGHT); g.DrawImage(tb, tx, ty); toffset[1]++; } toffset[0]++; toffset[1] = 0; } } args.Device.DrawImage(img, 0, 0); g.Dispose(); img.Dispose(); IsBusy = false; }下面两个方法用来计算有效的视野,给地图初始化之用,代码如下:
public Extent GetAvailableExtent(Coordinate center, Size rc) { var ext = new Extent(); var horizontal = resolutions[ZoomLevel] * rc.Width; var vertical = resolutions[ZoomLevel] * rc.Height; ext.MinX = center.X - horizontal / 2; ext.MinY = center.Y - vertical / 2; ext.MaxX = center.X + horizontal / 2; ext.MaxY = center.Y + vertical / 2; return ext; } public void GetDistance(double[] xy) { var resolution = resolutions[ZoomLevel]; xy[0] = xy[0] * resolution; xy[1] = xy[1] * resolution; }至此瓦片图图层完成。
DotSpatial地图控件默认没有比例尺,也就是自由比例尺,可以无限制的缩放。而在线地图只有18个缩放级别,如果不用地图函数限制DotSpatial地图控件的行为,就会导致地图移位。代码如下:
using DotSpatial.Controls; using DotSpatial.Topology; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace GoogleWmts { class TileMapFunction : MapFunction { private int zoomLevel = 11; private System.Drawing.Point firstPoint; private System.Drawing.Point lastPoint; private ResolutionLayer layer; public TileMapFunction(IMap mapCtrl, ResolutionLayer layer) : base(mapCtrl) { this.layer = layer; } protected override void OnMouseDown(GeoMouseArgs e) { firstPoint = e.Location; base.OnMouseDown(e); } protected override void OnMouseUp(GeoMouseArgs e) { lastPoint = e.Location; //var offset = new double[2] { firstPoint.X - lastPoint.X, firstPoint.Y - lastPoint.Y }; //layer.GetDistance(offset); //Map.ViewExtents.SetCenter(new Coordinate(Map.ViewExtents.Center.X + offset[0], Map.ViewExtents.Center.Y + offset[1])); base.OnMouseUp(e); } protected override void OnMouseWheel(GeoMouseArgs e) { e.Handled = true; if (layer.IsBusy) return; if (e.Delta > 0) zoomLevel++; else zoomLevel--; if (zoomLevel < 0) zoomLevel = 0; if (zoomLevel > ResolutionLayer.MAX_ZOOM_LEVEL) zoomLevel = ResolutionLayer.MAX_ZOOM_LEVEL; layer.ZoomLevel = zoomLevel; Console.WriteLine("中心点:" + Map.ViewExtents.Center.X + "," + Map.ViewExtents.Center.Y); //Map.ViewExtents = layer.GetAvailableExtent(Map.ViewExtents.Center, Map.ClientRectangle.Size); base.OnMouseWheel(e); } protected override void OnMouseMove(GeoMouseArgs e) { e.Handled = true; base.OnMouseMove(e); } } }
首先声明瓦片图图层与地图函数对象,加入到地图控件,代码如下:
private ResolutionLayer layer; private TileMapFunction func; layer = new ResolutionLayer() { Projection = currentProjection, WindowSize = mapCtrl.Size, ZoomLevel = 10 }; func = new TileMapFunction(mapCtrl, layer); mapCtrl.Layers.Add(layer); mapCtrl.Projection = currentProjection; mapCtrl.MapFunctions.Add(func); mapCtrl.ActivateMapFunction(func);
protected override void OnLoad(EventArgs e) { base.OnLoad(e); layer.WindowCreated = true; mapCtrl.ViewExtents = layer.GetAvailableExtent(wuhanCoordinate, layer.WindowSize); }处理窗品大小改变事件,使地图始终铺满窗口,代码如下:
protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); if(lastSize!=Size) { lastSize = Size; if (WindowState != FormWindowState.Minimized && mapCtrl.Width <= 0) { layer.WindowSize = this.Size; mapCtrl.Size = this.Size; Console.WriteLine("中心点:" + mapCtrl.ViewExtents.Center.X + "," + mapCtrl.ViewExtents.Center.Y); } } }
标签:
原文地址:http://blog.csdn.net/caoshiying/article/details/51991647