基于瓦片地图的控件
本控件使用dotnet编写,基于WPF的数据绑定自动生成,可以用于展示瓦片地图。为了提高地图加载速度,我们使用了内存缓存和本地文件缓存技术,并采用从中心扩散异步等加载方式。这些技术的结合,使得地图的加载更加流畅。
你可以在Nuget上搜索xyxandwxx.MapControl
直接引用该控件
多个瓦片组成地图控件
<UserControl x:Class="MapControl.Gis.GisLayout"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:MapControl.Gis"mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" Name="MainControl" Background="#00000000"><UserControl.Resources><ResourceDictionary><ResourceDictionary.MergedDictionaries><ResourceDictionary Source="Styles/GisMapGridStyle.xaml"></ResourceDictionary></ResourceDictionary.MergedDictionaries></ResourceDictionary></UserControl.Resources><Canvas Background="#00000000" Name="MapControlGrid" IsManipulationEnabled="True" ManipulationStarting="Grid_ManipulationStarting" ManipulationDelta="Grid_ManipulationDeltaAsync" ManipulationCompleted="Grid_ManipulationCompleted"><Grid Canvas.Left="{Binding OffsetX,ElementName=MainControl}" Canvas.Top="{Binding OffsetY,ElementName=MainControl}"MouseMove="MMove" MouseDown="MDown" MouseUp="MUp" MouseLeave="MLeave" MouseWheel="MWheel" ><ItemsControl ItemsSource="{Binding BaseMap.TitleBlocks,ElementName=MainControl}"ScrollViewer.CanContentScroll="False" Name="MapLayout"Style="{StaticResource MapCanvasImageList}"></ItemsControl><ItemsControl ItemsSource="{Binding Areas,ElementName=MainControl}"ScrollViewer.CanContentScroll="False"Style="{StaticResource MaskListStyle}"></ItemsControl></Grid></Canvas></UserControl>
单个瓦片数据结构
瓦片的数据结构,其中X、Y、Level是瓦片本身的属性,用于获取到瓦片图的路径Url,后面的高度Height、宽度Width就是显示为图像控件的大小,偏移量LayerOffsetPixelX与LayerOffsetPixelY就是基于地图左上角的瓦片,在右边第几个LayerOffsetPixelX就是几倍的
Width,在下面第几个LayerOffsetPixelY就是几倍的Height。
public class TitleBlock : INotifyPropertyChanged
{/// <summary>/// X/// </summary>public int TitleX { get; set; }/// <summary>/// Y/// </summary>public int TitleY { get; set; }/// <summary>/// 所在的地图等级/// </summary>public int Level { get; set; }/// <summary>/// 地址/// </summary>public Uri Url { get; set; }/// <summary>/// 宽度/// </summary>public double Width { get; set; } = 256;/// <summary>/// 高度/// </summary>public double Height { get; set; } = 256;/// <summary>/// 绘制的时候的偏移量/// </summary>public double LayerOffsetPixelX { get; set; }/// <summary>/// 绘制的时候的偏移量/// </summary>public double LayerOffsetPixelY { get; set; }public event PropertyChangedEventHandler? PropertyChanged;
}
瓦片的样式
<Style TargetType="ItemsControl" x:Key="MapCanvasImageList"><Setter Property="ItemsPanel"><Setter.Value><ItemsPanelTemplate><Canvas IsItemsHost="True" Width="{Binding BaseMap.PixelWidth,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=UserControl}}"Height="{Binding BaseMap.PixelHeight,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=UserControl}}"></Canvas></ItemsPanelTemplate></Setter.Value></Setter><Setter Property="ItemContainerStyle"><Setter.Value><Style TargetType="ContentPresenter"><Setter Property="Canvas.Left" Value="{Binding LayerOffsetPixelX}"></Setter><Setter Property="Canvas.Top" Value="{Binding LayerOffsetPixelY}"></Setter></Style></Setter.Value></Setter><Setter Property="ItemTemplate"><Setter.Value><DataTemplate><local:LoadingImage Width="{Binding Width}" Height="{Binding Height}" Source="{Binding Url,IsAsync=True}"></local:LoadingImage></DataTemplate></Setter.Value></Setter>
</Style>
地图布局的数据结构
/// <summary>
/// 基础的地图布局类
/// </summary>
public abstract class BaseMapLayout : INotifyPropertyChanged
{/// <summary>/// 当前地图的显示层级/// </summary>public virtual int Level { get; set; }/// <summary>/// 瓦片地图宽度/// </summary>public virtual double MapTitleWidth { get; set; } = 256;/// <summary>/// 最大和最小的显示等级/// </summary>public abstract int MinLevel { get; set; }public abstract int MaxLevel { get; set; }/// <summary>/// 当前的行数/// </summary>public int Rows { get; protected set; }/// <summary>/// 当前的列数/// </summary>public int Cols { get; protected set; }/// <summary>/// 总的像素宽度/// </summary>public double PixelWidth { get; set; }/// <summary>/// 总的像素高度/// </summary>public double PixelHeight { get; set; }/// <summary>/// 修改的次数/// </summary>public int ModifyCount { get; set; } = 0;/// <summary>/// 当前的偏移box/// </summary>public TitleBlock OffsetTitleBox { get; set; }/// <summary>/// 缓存管理/// </summary>internal CacheManager CacheManager { get; set; }/// <summary>/// 当前的block集合/// </summary>public virtual IList<TitleBlock> TitleBlocks { get; protected set; }/// <summary>/// 当前层一共有多少个区块/// </summary>public virtual int TotalBlock{get{var t = (int)Math.Pow(Math.Pow(2, Level), 2);if (t == 0) return 1;return t;}}
}
1.其中Level、MinLevel、MaxLevel是表示地图等级,一般是前端通过鼠标滚轮事件MouseWheel
、触摸屏双指操作更改ManipulationDelta
。
2.其中行数、列数,一般是前端通过按住鼠标移动、触摸屏滑动事件更改,其中触摸事件可以参考创建你的第一个触控应用程序。
自定义地图用户控件
这里主要是一些依赖属性
public partial class GisLayout : UserControl
{#region 扩展属性public static readonly DependencyProperty BaseMapProperty = DependencyProperty.Register("BaseMap", typeof(BaseMapLayout), typeof(GisLayout));public static DependencyProperty AreasProperty = DependencyProperty.Register("Areas", typeof(IEnumerable), typeof(GisLayout));public static DependencyProperty OffsetXProperty = DependencyProperty.Register("OffsetX", typeof(double), typeof(GisLayout));public static DependencyProperty OffsetYProperty = DependencyProperty.Register("OffsetY", typeof(double), typeof(GisLayout));public static DependencyProperty FillProperty = DependencyProperty.Register("Fill", typeof(Brush), typeof(GisLayout));public static DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(GisLayout));public static DependencyProperty CenterPointProperty = DependencyProperty.Register("CenterPoint", typeof(Point), typeof(GisLayout), new PropertyMetadata(CenterPointCallback));public static DependencyProperty IsZoomAutoCenterProperty = DependencyProperty.Register("IsZoomAutoCenter", typeof(bool), typeof(GisLayout), new PropertyMetadata(false));public static DependencyProperty TextFontSizeProperty = DependencyProperty.Register("TextFontSize", typeof(double), typeof(GisLayout), new PropertyMetadata(12.0));public static DependencyProperty LevelProperty = DependencyProperty.Register("Level", typeof(int), typeof(GisLayout), new PropertyMetadata(new PropertyChangedCallback(OnLevelChanged)));public static DependencyProperty MaxLevelProperty = DependencyProperty.Register("MaxLevel", typeof(int), typeof(GisLayout));public static readonly DependencyPropertyKey LevelMinusPropertyKey = DependencyProperty.RegisterReadOnly("LevelMinus", typeof(int), typeof(GisLayout), null);public static readonly DependencyProperty LevelMinusProperty = LevelMinusPropertyKey.DependencyProperty;public static DependencyProperty ItemVisibilityProperty = DependencyProperty.Register("ItemVisibility", typeof(Visibility), typeof(GisLayout));private static void CenterPointCallback(DependencyObject d, DependencyPropertyChangedEventArgs e){GisLayout layout = d as GisLayout;Point p = (Point)e.NewValue;layout.SetCenter(p.X, p.Y);}public static DependencyProperty DragMouseButtonProperty = DependencyProperty.Register("DragMouseButton", typeof(MouseButton), typeof(GisLayout), new PropertyMetadata(MouseButton.Right));#endregion/// <summary>/// Item的Visibility状态/// </summary>public Visibility ItemVisibility { get => (Visibility)GetValue(ItemVisibilityProperty); set => SetValue(ItemVisibilityProperty, value); }/// <summary>/// 地图最大Level与当前Level的差值/// </summary>public int LevelMinus { get => (int)GetValue(LevelMinusProperty); private set => SetValue(LevelMinusPropertyKey, value); }/// <summary>/// 地图当前Level/// </summary>public int Level { get => (int)GetValue(LevelProperty); set => SetValue(LevelProperty, value); }/// <summary>/// 地图最大Level/// </summary>public int MaxLevel { get => (int)GetValue(MaxLevelProperty); set => SetValue(MaxLevelProperty, value); }/// <summary>/// 文本字体大小/// </summary>public double TextFontSize { get => (double)GetValue(TextFontSizeProperty); set => SetValue(TextFontSizeProperty, value); }/// <summary>/// 设置缩放自动更新中心点/// </summary>public bool IsZoomAutoCenter { get => (bool)GetValue(IsZoomAutoCenterProperty); set => SetValue(IsZoomAutoCenterProperty, value); }/// <summary>/// 地图操作类/// </summary>public BaseMapLayout BaseMap { get => (BaseMapLayout)GetValue(BaseMapProperty); set => SetValue(BaseMapProperty, value); }/// <summary>/// 经纬度区域数据集/// </summary>public IEnumerable Areas { get => (IEnumerable)GetValue(AreasProperty); set => SetValue(AreasProperty, value); }/// <summary>/// 控件偏移坐标/// </summary>public double OffsetX { get => (double)GetValue(OffsetXProperty); set => SetValue(OffsetXProperty, value); }/// <summary>/// 控件拖拽偏移坐标/// </summary>public double OffsetY { get => (double)GetValue(OffsetYProperty); set => SetValue(OffsetYProperty, value); }/// <summary>/// 经纬度遮罩填充颜色/// </summary>public Brush Fill { get => (Brush)GetValue(FillProperty); set => SetValue(FillProperty, value); }/// <summary>/// 中心经纬度/// </summary>public Point CenterPoint { get => (Point)GetValue(CenterPointProperty); set => SetValue(CenterPointProperty, value); }/// <summary>/// 拖拽鼠标的按键/// </summary>public MouseButton DragMouseButton { get => (MouseButton)GetValue(DragMouseButtonProperty); set => SetValue(DragMouseButtonProperty, value); }/// <summary>/// 子项模板/// </summary>public DataTemplate ItemTemplate { get => (DataTemplate)GetValue(ItemTemplateProperty); set => SetValue(ItemTemplateProperty, value); }/// <summary>/// 构造函数/// </summary>public GisLayout(){InitializeComponent();this.Loaded += GisLayout_Loaded;}private void GisLayout_Loaded(object sender, RoutedEventArgs e){SetCenter(CenterPoint.X, CenterPoint.Y);}public event PropertyChangedEventHandler PropertyChanged;private static void OnLevelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){int maxLevel = (int)d.GetValue(MaxLevelProperty);int level = (int)d.GetValue(LevelProperty);d.SetValue(LevelMinusPropertyKey, maxLevel - level);}protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo){base.OnRenderSizeChanged(sizeInfo);SetCenter(CenterPoint.X, CenterPoint.Y);}/// <summary>/// 经纬度坐标转当前地图的像素坐标/// </summary>/// <param name="mapPoint">经纬度坐标</param>/// <returns>像素坐标</returns>private Point ConvertToPixelPoint(Point mapPoint){if (BaseMap is null) return new Point();PixelTitleBlock block = BaseMap.LongitudeAndAtitudeConvertToPixel(mapPoint.X, mapPoint.Y);var centerX = (block.TitleX - BaseMap.OffsetTitleBox.TitleX) * block.Width + block.OffsetX;var centerY = (block.TitleY - BaseMap.OffsetTitleBox.TitleY) * block.Height + block.OffsetY;return new Point(centerX, centerY);}/// <summary>/// 设置居中 在设置的经纬度/// </summary>/// <param name="lat">维度</param>/// <param name="lng">经度</param>public void SetCenter(double lng, double lat){if (BaseMap is null) return;var center = ConvertToPixelPoint(new Point(lng, lat));var offsetx = RenderSize.Width / 2 - center.X;var offsety = RenderSize.Height / 2 - center.Y;var clip = OffsetClip(new Point(offsetx, offsety));OffsetX = clip.X;OffsetY = clip.Y;}#region 鼠标控制/// <summary>/// 前一个坐标/// </summary>private Point ForntPoint { get; set; }/// <summary>/// 是否移动/// </summary>private bool IsMove { get; set; }/// <summary>/// 是否是多指操作/// </summary>private bool IsManipulationOption = false;#region 移动控制/// <summary>/// 偏移裁剪过滤 防止超出边界/// </summary>/// <param name="target">目标偏移</param>/// <returns>实际偏移</returns>private Point OffsetClip(Point target){var tempX = target.X;var tempY = target.Y;if (tempX > 0){tempX = 0;}else if (tempX <= -BaseMap.PixelWidth + this.RenderSize.Width){tempX = -BaseMap.PixelWidth + this.RenderSize.Width;}if (tempY > 0){tempY = 0;}else if (tempY <= -BaseMap.PixelHeight + this.RenderSize.Height){tempY = -BaseMap.PixelHeight + this.RenderSize.Height;}return new Point(tempX, tempY);}/// <summary>/// 计算2个点的距离/// </summary>/// <param name="p1"></param>/// <param name="p2"></param>/// <returns></returns>private double GetDistance(Point p1, Point p2){return Math.Sqrt(Math.Pow(p1.X - p2.X, 2) + Math.Pow(p1.Y - p2.Y, 2));}/// <summary>/// 开始移动/// </summary>/// <param name="point"></param>private void StartMove(Point point){IsMove = true;ForntPoint = point;}/// <summary>/// 停止移动/// </summary>private void StopMove(){IsMove = false;}/// <summary>/// 使用新的点更新移动/// </summary>/// <param name="point"></param>private void UpdateMove(Func<Point> getCurrentPoint){if (!IsMove || IsManipulationOption) return;Point now = getCurrentPoint();var x = now.X - ForntPoint.X;var y = now.Y - ForntPoint.Y;var dis = Math.Sqrt(x * x + y * y);if (dis < 3){return;}if (dis > 200){return;}var tempX = OffsetX + x;var tempY = OffsetY + y;var clip = OffsetClip(new Point(tempX, tempY));OffsetX = clip.X;OffsetY = clip.Y;ForntPoint = now;}/// <summary>/// 使用新的点更新移动/// </summary>/// <param name="point"></param>private void UpdateMove(Vector offset){var x = offset.X;var y = offset.Y;var dis = Math.Sqrt(x * x + y * y);if(dis < 2){return;}if (dis > 200){return;}var tempX = OffsetX + x;var tempY = OffsetY + y;var clip = OffsetClip(new Point(tempX, tempY));OffsetX = clip.X;OffsetY = clip.Y;}private bool IsZooming = false; // 是否正在缩放private DateTime ForntTime = DateTime.Now; // 上次操作的事件private long SpanMiliseconds = 1000; // 间隔的毫秒/// <summary>/// 更新缩放/// </summary>/// <param name="isBigger">是变大还是缩小</param>/// <param name="zoomCenterPoint">缩放中心点</param>private async Task UpdateZoom(bool isBigger, Point zoomCenterPoint){if (IsZooming) return;if ((DateTime.Now - ForntTime).TotalMilliseconds < SpanMiliseconds) return;IsZooming = true;var point = zoomCenterPoint;var lngLat = BaseMap.PixelConvertToLongitudeAndAtitude(point.X, point.Y);if (!isBigger){await BaseMap.ResetLayout(lngLat.X, lngLat.Y, BaseMap.Level - 1);}else{await BaseMap.ResetLayout(lngLat.X, lngLat.Y, BaseMap.Level + 1);}SetCenter(lngLat.X, lngLat.Y);IsZooming = false;ForntTime = DateTime.Now;}#endregion#region 鼠标控制的移动private void MMove(object sender, MouseEventArgs e){UpdateMove(() => e.GetPosition(MapControlGrid));}private void MDown(object sender, MouseButtonEventArgs e){if (e.ChangedButton == DragMouseButton){StartMove(e.GetPosition(MapControlGrid));}}private void MUp(object sender, MouseButtonEventArgs e){StopMove();}private void MLeave(object sender, MouseEventArgs e){StopMove();}#endregion#region 鼠标控制的缩放/// <summary>/// 注册了滚轮放大事件/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private async void MWheel(object sender, MouseWheelEventArgs e){var res = e.Delta;var point = e.GetPosition(sender as FrameworkElement);await UpdateZoom(res > 0, IsZoomAutoCenter ? point : ConvertToPixelPoint(CenterPoint));}#endregion/// <summary>/// 双指操作/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private async void Grid_ManipulationDeltaAsync(object sender, ManipulationDeltaEventArgs e){if (e.Manipulators.Count() < 2){IsManipulationOption = false;UpdateMove(e.DeltaManipulation.Translation);return;}var point1 = e.Manipulators.First().GetPosition(MapLayout);var point2 = e.Manipulators.Last().GetPosition(MapLayout);var point = new Point((point1.X + point2.X) / 2, (point1.Y + point2.Y) / 2);IsManipulationOption = true;var scale = e.DeltaManipulation.Scale;if (scale.X < 1 && scale.Y < 1){await UpdateZoom(false, IsZoomAutoCenter ? point : ConvertToPixelPoint(CenterPoint));}else if (scale.X > 1 && scale.Y > 1){await UpdateZoom(true, IsZoomAutoCenter ? point : ConvertToPixelPoint(CenterPoint));}IsManipulationOption = false;}/// <summary>/// 多指操作开始/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void Grid_ManipulationStarting(object sender, ManipulationStartingEventArgs e){e.ManipulationContainer = sender as FrameworkElement;e.Mode = ManipulationModes.All;}/// <summary>/// 多指操作结束/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void Grid_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e){IsManipulationOption = false;}#endregion}
高德瓦片
高德地图的瓦片获取方式,前者lang可以通过zh_cn设置中文,en设置英文,size基本无作用,scl设置标注还是底图,scl=1代表注记,scl=2代表底图(矢量或者影像),style设置影像和路网,style=6为影像图,style=7为矢量路网,style=8为影像路网
public override string GetUri(int row, int column, int level)
{if (MapImage == MapImageType.RoadNetwork)return "http://webrd0" + (column % 4 + 1) + ".is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x=" + column + "&y=" + row + "&z=" + level;elsereturn "http://webst0" + (column % 4 + 1) + ".is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=6&x=" + column + "&y=" + row + "&z=" + level;
}
外部调用地图控件
<map:GisLayout BaseMap="{Binding Path=BaseMapLayout}" Fill="#50FF0000" DragMouseButton="Left"Areas="{Binding Path=IotBlockList}" CenterPoint="{Binding CenterPoint}"Level="{Binding BaseMapLayout.Level,Mode=OneWay}" MaxLevel="{Binding BaseMapLayout.MaxLevel,Mode=OneWay}"x:Name="Map" IsZoomAutoCenter="True"><map:GisLayout.Style><Style TargetType="{x:Type map:GisLayout}"><Style.Triggers><Trigger Property="LevelMinus" Value="0"><Setter Property="TextFontSize" Value="20"/></Trigger><Trigger Property="LevelMinus" Value="1"><Setter Property="TextFontSize" Value="15"/></Trigger><Trigger Property="LevelMinus" Value="2"><Setter Property="ItemVisibility" Value="Collapsed"/></Trigger></Style.Triggers></Style></map:GisLayout.Style><map:GisLayout.ItemTemplate><DataTemplate><StackPanel Orientation="Vertical" Visibility="{Binding Path=ItemVisibility, RelativeSource={RelativeSource AncestorType=map:GisLayout}}"><behaviour:Interaction.Triggers><behaviour:EventTrigger EventName="MouseLeftButtonDown"><behaviour:InvokeCommandAction Command="{Binding ClickCommand}"CommandParameter="{Binding Path=.}"/></behaviour:EventTrigger><behaviour:EventTrigger EventName="TouchDown"><behaviour:InvokeCommandAction Command="{Binding ClickCommand}"CommandParameter="{Binding Path=.}"/></behaviour:EventTrigger></behaviour:Interaction.Triggers><Image Source="../Resources/定位.png" Stretch="None" IsHitTestVisible="False"/><TextBlock Text="{Binding Name}" Foreground="Black" FontSize="{Binding Path=TextFontSize, RelativeSource={RelativeSource AncestorType=map:GisLayout}}" HorizontalAlignment="Center" VerticalAlignment="Center" IsHitTestVisible="False"/></StackPanel></DataTemplate></map:GisLayout.ItemTemplate>
</map:GisLayout>