Version 2.3.0:

- Added BingMapsTileLayer
- Added TileLayer.DescriptionInlines property
- Added global Settings class
- Added Phone Silverlight 8.1 build target
- Use expiration time of downloaded images for caching
This commit is contained in:
ClemensF 2014-10-19 21:50:23 +02:00
parent 8917e1d4cb
commit 91ff46c506
58 changed files with 1225 additions and 491 deletions

View file

@ -0,0 +1,153 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Xml;
#if WINDOWS_RUNTIME
using Windows.UI.Xaml;
#else
using System.Windows;
#endif
namespace MapControl
{
public class BingMapsTileLayer : TileLayer
{
public enum MapMode
{
Road, Aerial, AerialWithLabels
}
public BingMapsTileLayer()
{
MinZoomLevel = 1;
MaxZoomLevel = 21;
Loaded += OnLoaded;
}
public static string ApiKey { get; set; }
public MapMode Mode { get; set; }
public string Culture { get; set; }
private void OnLoaded(object sender, RoutedEventArgs e)
{
Loaded -= OnLoaded;
if (string.IsNullOrWhiteSpace(ApiKey))
{
throw new InvalidOperationException("A Bing Maps API Key must be assigned to the ApiKey property.");
}
var uri = string.Format("http://dev.virtualearth.net/REST/V1/Imagery/Metadata/{0}?output=xml&key={1}", Mode, ApiKey);
var request = HttpWebRequest.CreateHttp(uri);
request.BeginGetResponse(HandleImageryMetadataResponse, request);
}
private void HandleImageryMetadataResponse(IAsyncResult asyncResult)
{
try
{
var request = (HttpWebRequest)asyncResult.AsyncState;
using (var response = request.EndGetResponse(asyncResult))
using (var responseStream = response.GetResponseStream())
using (var xmlReader = XmlReader.Create(responseStream))
{
ReadImageryMetadataResponse(xmlReader);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
private void ReadImageryMetadataResponse(XmlReader xmlReader)
{
string imageUrl = null;
string[] imageUrlSubdomains = null;
int? zoomMin = null;
int? zoomMax = null;
do
{
if (xmlReader.NodeType == XmlNodeType.Element)
{
switch (xmlReader.Name)
{
case "ImageUrl":
imageUrl = xmlReader.ReadElementContentAsString();
break;
case "ImageUrlSubdomains":
imageUrlSubdomains = ReadStrings(xmlReader.ReadSubtree());
break;
case "ZoomMin":
zoomMin = xmlReader.ReadElementContentAsInt();
break;
case "ZoomMax":
zoomMax = xmlReader.ReadElementContentAsInt();
break;
default:
xmlReader.Read();
break;
}
}
else
{
xmlReader.Read();
}
}
while (xmlReader.NodeType != XmlNodeType.None);
if (imageUrl != null && imageUrlSubdomains != null && imageUrlSubdomains.Length > 0)
{
Dispatcher.BeginInvoke(new Action(() =>
{
if (string.IsNullOrWhiteSpace(Culture))
{
Culture = CultureInfo.CurrentUICulture.Name;
}
TileSource = new BingMapsTileSource(imageUrl.Replace("{culture}", Culture), imageUrlSubdomains);
if (zoomMin.HasValue && zoomMin.Value > MinZoomLevel)
{
MinZoomLevel = zoomMin.Value;
}
if (zoomMax.HasValue && zoomMax.Value < MaxZoomLevel)
{
MaxZoomLevel = zoomMax.Value;
}
}));
}
}
private static string[] ReadStrings(XmlReader xmlReader)
{
var strings = new List<string>();
do
{
if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.Name == "string")
{
strings.Add(xmlReader.ReadElementContentAsString());
}
else
{
xmlReader.Read();
}
}
while (xmlReader.NodeType != XmlNodeType.None);
return strings.ToArray();
}
}
}

View file

@ -0,0 +1,39 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
namespace MapControl
{
internal class BingMapsTileSource : TileSource
{
private string[] subdomains;
public BingMapsTileSource(string uriFormat, string[] subdomains)
: base(uriFormat)
{
this.subdomains = subdomains;
}
public override Uri GetUri(int x, int y, int zoomLevel)
{
if (zoomLevel < 1)
{
return null;
}
var subdomain = subdomains[(x + y) % subdomains.Length];
var quadkey = new char[zoomLevel];
for (var z = zoomLevel - 1; z >= 0; z--, x /= 2, y /= 2)
{
quadkey[z] = (char)('0' + 2 * (y % 2) + (x % 2));
}
return new Uri(UriFormat.
Replace("{subdomain}", subdomain).
Replace("{quadkey}", new string(quadkey)));
}
}
}

View file

@ -13,10 +13,6 @@ namespace MapControl
{
internal static partial class Extensions
{
public static void Freeze(this object freezable)
{
}
public static Matrix Translate(this Matrix matrix, double offsetX, double offsetY)
{
matrix.OffsetX += offsetX;

View file

@ -2,7 +2,9 @@
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using Windows.Foundation;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
@ -11,6 +13,11 @@ namespace MapControl
{
internal static partial class Extensions
{
public static void BeginInvoke(this CoreDispatcher dispatcher, Action action)
{
var asyncAction = dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(action));
}
public static Point Transform(this GeneralTransform transform, Point point)
{
return transform.TransformPoint(point);

View file

@ -19,7 +19,7 @@ namespace MapControl
if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
{
throw new ArgumentException(string.Format("{0}: no GlyphTypeface found", typeface.FontFamily));
throw new ArgumentException(string.Format("{0}: No GlyphTypeface found", typeface.FontFamily));
}
var glyphIndices = new ushort[text.Length];

View file

@ -0,0 +1,60 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
#if WINDOWS_RUNTIME
using Windows.UI.Xaml.Documents;
#else
using System.Windows.Documents;
#endif
namespace MapControl
{
public static class HyperlinkText
{
private static Regex regex = new Regex(@"\[([^\]]+)\]\(([^\)]+)\)");
/// <summary>
/// Converts text containing hyperlinks in markdown syntax [text](url)
/// to a collection of Run and Hyperlink inlines.
/// </summary>
public static ICollection<Inline> ToInlines(this string text)
{
var inlines = new List<Inline>();
while (!string.IsNullOrEmpty(text))
{
var match = regex.Match(text);
Uri uri;
if (match.Success &&
match.Groups.Count == 3 &&
Uri.TryCreate(match.Groups[2].Value, UriKind.Absolute, out uri))
{
inlines.Add(new Run { Text = text.Substring(0, match.Index) });
text = text.Substring(match.Index + match.Length);
var link = new Hyperlink { NavigateUri = uri };
link.Inlines.Add(new Run { Text = match.Groups[1].Value });
#if SILVERLIGHT
link.TargetName = "_blank";
#elif !WINDOWS_RUNTIME
link.ToolTip = uri.ToString();
link.RequestNavigate += (s, e) => System.Diagnostics.Process.Start(e.Uri.ToString());
#endif
inlines.Add(link);
}
else
{
inlines.Add(new Run { Text = text });
text = null;
}
}
return inlines;
}
}
}

View file

@ -4,7 +4,7 @@
namespace MapControl
{
public struct Int32Rect
internal struct Int32Rect
{
public Int32Rect(int x, int y, int width, int height)
: this()

View file

@ -28,9 +28,6 @@ namespace MapControl
{
private const double MaximumZoomLevel = 22d;
public static TimeSpan AnimationDuration = TimeSpan.FromSeconds(0.5);
public static EasingFunctionBase AnimationEasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut };
public static readonly DependencyProperty TileLayersProperty = DependencyProperty.Register(
"TileLayers", typeof(TileLayerCollection), typeof(MapBase), new PropertyMetadata(null,
(o, e) => ((MapBase)o).TileLayersPropertyChanged((TileLayerCollection)e.OldValue, (TileLayerCollection)e.NewValue)));
@ -74,7 +71,7 @@ namespace MapControl
SetParentMap();
TileLayers = new TileLayerCollection();
InternalChildren.Add(tileContainer);
Children.Add(tileContainer);
Initialize();
Loaded += OnLoaded;
@ -578,8 +575,8 @@ namespace MapControl
{
From = MapTransform.Transform(Center),
To = MapTransform.Transform(targetCenter, Center.Longitude),
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction,
Duration = Settings.MapAnimationDuration,
EasingFunction = Settings.MapAnimationEasingFunction,
FillBehavior = FillBehavior.HoldEnd
};
@ -683,8 +680,8 @@ namespace MapControl
zoomLevelAnimation = new DoubleAnimation
{
To = targetZoomLevel,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction,
Duration = Settings.MapAnimationDuration,
EasingFunction = Settings.MapAnimationEasingFunction,
FillBehavior = FillBehavior.HoldEnd
};
@ -758,8 +755,8 @@ namespace MapControl
headingAnimation = new DoubleAnimation
{
By = delta,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction,
Duration = Settings.MapAnimationDuration,
EasingFunction = Settings.MapAnimationEasingFunction,
FillBehavior = FillBehavior.HoldEnd
};

View file

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>10.0.20506</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{3499D618-2846-4FCE-A418-7D211FDBDCB3}</ProjectGuid>
<ProjectTypeGuids>{C089C8C0-30E0-4E22-80C0-CE093F111A43};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MapControl</RootNamespace>
<AssemblyName>MapControl.PhoneSilverlight</AssemblyName>
<TargetFrameworkIdentifier>WindowsPhone</TargetFrameworkIdentifier>
<TargetFrameworkVersion>v8.1</TargetFrameworkVersion>
<SilverlightVersion>$(TargetFrameworkVersion)</SilverlightVersion>
<SilverlightApplication>false</SilverlightApplication>
<ValidateXaml>true</ValidateXaml>
<MinimumVisualStudioVersion>12.0</MinimumVisualStudioVersion>
<ThrowErrorsInValidation>true</ThrowErrorsInValidation>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>Bin\Debug</OutputPath>
<DefineConstants>DEBUG;TRACE;SILVERLIGHT;WINDOWS_PHONE</DefineConstants>
<NoStdLib>true</NoStdLib>
<NoConfig>true</NoConfig>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>none</DebugType>
<Optimize>true</Optimize>
<OutputPath>Bin\Release</OutputPath>
<DefineConstants>TRACE;SILVERLIGHT;WINDOWS_PHONE</DefineConstants>
<NoStdLib>true</NoStdLib>
<NoConfig>true</NoConfig>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>Bin\x86\Debug</OutputPath>
<DefineConstants>DEBUG;TRACE;SILVERLIGHT;WINDOWS_PHONE</DefineConstants>
<NoStdLib>true</NoStdLib>
<NoConfig>true</NoConfig>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>Bin\x86\Release</OutputPath>
<DefineConstants>TRACE;SILVERLIGHT;WINDOWS_PHONE</DefineConstants>
<NoStdLib>true</NoStdLib>
<NoConfig>true</NoConfig>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|ARM' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>Bin\ARM\Debug</OutputPath>
<DefineConstants>DEBUG;TRACE;SILVERLIGHT;WINDOWS_PHONE</DefineConstants>
<NoStdLib>true</NoStdLib>
<NoConfig>true</NoConfig>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|ARM' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>Bin\ARM\Release</OutputPath>
<DefineConstants>TRACE;SILVERLIGHT;WINDOWS_PHONE</DefineConstants>
<NoStdLib>true</NoStdLib>
<NoConfig>true</NoConfig>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Compile Include="BingMapsTileLayer.cs" />
<Compile Include="BingMapsTileSource.cs" />
<Compile Include="Extensions.Silverlight.cs" />
<Compile Include="Extensions.Silverlight.WinRT.cs" />
<Compile Include="HyperlinkText.cs" />
<Compile Include="ImageTileSource.Silverlight.WinRT.cs" />
<Compile Include="IMapElement.cs" />
<Compile Include="Int32Rect.cs" />
<Compile Include="Location.cs" />
<Compile Include="LocationCollection.cs" />
<Compile Include="LocationCollectionConverter.cs" />
<Compile Include="LocationConverter.cs" />
<Compile Include="Map.Silverlight.cs" />
<Compile Include="MapBase.cs" />
<Compile Include="MapBase.Silverlight.WinRT.cs" />
<Compile Include="MapGraticule.cs" />
<Compile Include="MapGraticule.Silverlight.WinRT.cs" />
<Compile Include="MapImage.cs" />
<Compile Include="MapImageLayer.cs" />
<Compile Include="MapImageLayer.Silverlight.WinRT.cs" />
<Compile Include="MapItem.Silverlight.WinRT.cs" />
<Compile Include="MapItemsControl.Silverlight.WinRT.cs" />
<Compile Include="MapOverlay.cs" />
<Compile Include="MapOverlay.Silverlight.WinRT.cs" />
<Compile Include="MapPanel.cs" />
<Compile Include="MapPanel.Silverlight.WinRT.cs" />
<Compile Include="MapPath.cs" />
<Compile Include="MapPath.Silverlight.WinRT.cs" />
<Compile Include="MapPolyline.cs" />
<Compile Include="MapPolyline.Silverlight.WinRT.cs" />
<Compile Include="MapRectangle.cs" />
<Compile Include="MapRectangle.Silverlight.WinRT.cs" />
<Compile Include="MapTransform.cs" />
<Compile Include="MercatorTransform.cs" />
<Compile Include="PanelBase.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Pushpin.Silverlight.WinRT.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Tile.cs" />
<Compile Include="Tile.Silverlight.WinRT.cs" />
<Compile Include="TileContainer.cs" />
<Compile Include="TileContainer.Silverlight.WinRT.cs" />
<Compile Include="TileImageLoader.Silverlight.cs" />
<Compile Include="TileLayer.cs" />
<Compile Include="TileLayerCollection.cs" />
<Compile Include="TileSource.cs" />
<Compile Include="TileSourceConverter.cs" />
</ItemGroup>
<ItemGroup>
<Page Include="Themes\Generic.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\$(TargetFrameworkIdentifier)\$(TargetFrameworkVersion)\Microsoft.$(TargetFrameworkIdentifier).$(TargetFrameworkVersion).Overrides.targets" />
<Import Project="$(MSBuildExtensionsPath)\Microsoft\$(TargetFrameworkIdentifier)\$(TargetFrameworkVersion)\Microsoft.$(TargetFrameworkIdentifier).CSharp.targets" />
<ProjectExtensions />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{C089C8C0-30E0-4E22-80C0-CE093F111A43}">
<SilverlightMobileCSProjectFlavor>
<FullDeploy>True</FullDeploy>
<DebuggerType>Managed</DebuggerType>
<DebuggerAgentType>Managed</DebuggerAgentType>
<Tombstone>False</Tombstone>
</SilverlightMobileCSProjectFlavor>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View file

@ -65,10 +65,14 @@
<HintPath>$(TargetFrameworkDirectory)System.Core.dll</HintPath>
</Reference>
<Reference Include="System.Net" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="BingMapsTileLayer.cs" />
<Compile Include="BingMapsTileSource.cs" />
<Compile Include="Extensions.Silverlight.cs" />
<Compile Include="Extensions.Silverlight.WinRT.cs" />
<Compile Include="HyperlinkText.cs" />
<Compile Include="ImageTileSource.Silverlight.WinRT.cs">
<SubType>Code</SubType>
</Compile>
@ -97,11 +101,13 @@
<Compile Include="MapPolyline.cs" />
<Compile Include="MapPolyline.Silverlight.WinRT.cs" />
<Compile Include="MapRectangle.cs" />
<Compile Include="MapRectangle.Silverlight.WinRT.cs" />
<Compile Include="MapTransform.cs" />
<Compile Include="MercatorTransform.cs" />
<Compile Include="PanelBase.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Pushpin.Silverlight.WinRT.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Tile.cs" />
<Compile Include="Tile.Silverlight.WinRT.cs" />
<Compile Include="TileContainer.cs" />

View file

@ -48,10 +48,14 @@
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Caching" />
<Reference Include="System.Xaml" />
<Reference Include="System.XML" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="BingMapsTileLayer.cs" />
<Compile Include="BingMapsTileSource.cs" />
<Compile Include="GlyphRunText.cs" />
<Compile Include="HyperlinkText.cs" />
<Compile Include="ImageTileSource.WPF.cs" />
<Compile Include="IMapElement.cs" />
<Compile Include="Location.cs" />
@ -72,6 +76,7 @@
<Compile Include="MapOverlay.WPF.cs" />
<Compile Include="MapPanel.cs" />
<Compile Include="MapPanel.WPF.cs" />
<Compile Include="MapRectangle.WPF.cs" />
<Compile Include="PanelBase.cs" />
<Compile Include="MapPath.cs" />
<Compile Include="MapPath.WPF.cs" />
@ -83,6 +88,7 @@
<Compile Include="MercatorTransform.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Pushpin.WPF.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Tile.cs" />
<Compile Include="Tile.WPF.cs" />
<Compile Include="TileContainer.cs" />

View file

@ -29,16 +29,10 @@ namespace MapControl
private void SourceChanged(ImageSource image)
{
var transform = new MatrixTransform
{
Matrix = new Matrix(1d, 0d, 0d, -1d, 0d, 1d)
};
transform.Freeze();
Fill = new ImageBrush
{
ImageSource = image,
RelativeTransform = transform
RelativeTransform = FillTransform
};
}
}

View file

@ -8,16 +8,13 @@ using Windows.UI.Xaml.Media.Imaging;
#else
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
#endif
namespace MapControl
{
public partial class MapImageLayer
{
private readonly DispatcherTimer updateTimer = new DispatcherTimer();
private void AddDownloadEventHandlers(BitmapSource bitmap)
private void ImageUpdated(BitmapSource bitmap)
{
var bitmapImage = bitmap as BitmapImage;
@ -35,7 +32,6 @@ namespace MapControl
private void BitmapImageOpened(object sender, RoutedEventArgs e)
{
var bitmap = (BitmapImage)sender;
bitmap.ImageOpened -= BitmapImageOpened;
bitmap.ImageFailed -= BitmapImageFailed;
@ -45,11 +41,12 @@ namespace MapControl
private void BitmapImageFailed(object sender, ExceptionRoutedEventArgs e)
{
var bitmap = (BitmapImage)sender;
bitmap.ImageOpened -= BitmapImageOpened;
bitmap.ImageFailed -= BitmapImageFailed;
((MapImage)Children[currentImageIndex]).Source = null;
var mapImage = (MapImage)Children[currentImageIndex];
mapImage.Source = null;
BlendImages();
}
}

View file

@ -5,17 +5,14 @@
using System;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace MapControl
{
public partial class MapImageLayer
{
private readonly DispatcherTimer updateTimer = new DispatcherTimer(DispatcherPriority.Background);
private void AddDownloadEventHandlers(BitmapSource bitmap)
private void ImageUpdated(BitmapSource bitmap)
{
if (bitmap.IsDownloading)
if (bitmap != null && !bitmap.IsFrozen && bitmap.IsDownloading)
{
bitmap.DownloadCompleted += BitmapDownloadCompleted;
bitmap.DownloadFailed += BitmapDownloadFailed;
@ -29,7 +26,6 @@ namespace MapControl
private void BitmapDownloadCompleted(object sender, EventArgs e)
{
var bitmap = (BitmapSource)sender;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
@ -39,11 +35,12 @@ namespace MapControl
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)
{
var bitmap = (BitmapSource)sender;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
((MapImage)Children[currentImageIndex]).Source = null;
var mapImage = (MapImage)Children[currentImageIndex];
mapImage.Source = null;
BlendImages();
}
}

View file

@ -13,14 +13,14 @@ using Windows.UI.Xaml.Media.Animation;
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
#endif
namespace MapControl
{
/// <summary>
/// Map image overlay. Fills the entire viewport with a map image from a web request,
/// for example from a Web Map Service (WMS).
/// The image request Uri is specified by the UriFormat property.
/// Map image overlay. Fills the entire viewport with map images provided by a web service,
/// e.g. a Web Map Service (WMS). The image request Uri is specified by the UriFormat property.
/// </summary>
public partial class MapImageLayer : MapPanel
{
@ -31,6 +31,7 @@ namespace MapControl
public static readonly DependencyProperty RelativeImageSizeProperty = DependencyProperty.Register(
"RelativeImageSize", typeof(double), typeof(MapImageLayer), new PropertyMetadata(1d));
private readonly DispatcherTimer updateTimer;
private int currentImageIndex;
private bool updateInProgress;
@ -39,15 +40,15 @@ namespace MapControl
Children.Add(new MapImage { Opacity = 0d });
Children.Add(new MapImage { Opacity = 0d });
updateTimer.Interval = TileContainer.UpdateInterval;
updateTimer = new DispatcherTimer { Interval = Settings.TileUpdateInterval };
updateTimer.Tick += (s, e) => UpdateImage();
}
/// <summary>
/// The format string of the image request Uri. The format must contain {X} and {Y}
/// format specifiers for the map width and height in pixels, and either
/// {w},{s},{e},{n} for the bounding box in lat/lon (like for example EPSG:4326) or
/// {W},{S},{E},{N} for the bounding box in meters (like for example EPSG:3857).
/// The format string of the image request Uri. The format must contain
/// {X} and {Y} format specifiers for the map width and height in pixels and either
/// {w},{s},{e},{n} for the bounding box in lat/lon (like EPSG:4326) or
/// {W},{S},{E},{N} for the bounding box in meters (like EPSG:3857).
/// </summary>
public string UriFormat
{
@ -74,10 +75,46 @@ namespace MapControl
updateTimer.Start();
}
protected virtual BitmapSource GetBitmap(double west, double east, double south, double north, int width, int height)
protected void UpdateImage()
{
BitmapImage image = null;
updateTimer.Stop();
if (updateInProgress)
{
updateTimer.Start(); // update image on next timer tick
}
else if (ParentMap != null && RenderSize.Width > 0 && RenderSize.Height > 0)
{
updateInProgress = true;
var relativeSize = Math.Max(RelativeImageSize, 1d);
var width = RenderSize.Width * relativeSize;
var height = RenderSize.Height * relativeSize;
var dx = (RenderSize.Width - width) / 2d;
var dy = (RenderSize.Height - height) / 2d;
var loc1 = ParentMap.ViewportPointToLocation(new Point(dx, dy));
var loc2 = ParentMap.ViewportPointToLocation(new Point(dx + width, dy));
var loc3 = ParentMap.ViewportPointToLocation(new Point(dx, dy + height));
var loc4 = ParentMap.ViewportPointToLocation(new Point(dx + width, dy + height));
var west = Math.Min(loc1.Longitude, Math.Min(loc2.Longitude, Math.Min(loc3.Longitude, loc4.Longitude)));
var east = Math.Max(loc1.Longitude, Math.Max(loc2.Longitude, Math.Max(loc3.Longitude, loc4.Longitude)));
var south = Math.Min(loc1.Latitude, Math.Min(loc2.Latitude, Math.Min(loc3.Latitude, loc4.Latitude)));
var north = Math.Max(loc1.Latitude, Math.Max(loc2.Latitude, Math.Max(loc3.Latitude, loc4.Latitude)));
var p1 = ParentMap.MapTransform.Transform(new Location(south, west));
var p2 = ParentMap.MapTransform.Transform(new Location(north, east));
width = Math.Round((p2.X - p1.X) * ParentMap.ViewportScale);
height = Math.Round((p2.Y - p1.Y) * ParentMap.ViewportScale);
UpdateImage(west, east, south, north, (int)width, (int)height);
}
}
protected virtual void UpdateImage(double west, double east, double south, double north, int width, int height)
{
if (UriFormat != null && width > 0 && height > 0)
{
var uri = UriFormat.Replace("{X}", width.ToString()).Replace("{Y}", height.ToString());
@ -102,98 +139,56 @@ namespace MapControl
Replace("{n}", north.ToString(CultureInfo.InvariantCulture));
}
image = new BitmapImage(new Uri(uri));
}
return image;
}
protected void UpdateImage()
{
if (updateInProgress)
{
updateTimer.Start(); // update image on next timer tick
UpdateImage(west, east, south, north, new Uri(uri));
}
else
{
updateTimer.Stop();
if (ParentMap != null && RenderSize.Width > 0 && RenderSize.Height > 0)
{
updateInProgress = true;
var relativeSize = Math.Max(RelativeImageSize, 1d);
var width = RenderSize.Width * relativeSize;
var height = RenderSize.Height * relativeSize;
var dx = (RenderSize.Width - width) / 2d;
var dy = (RenderSize.Height - height) / 2d;
var loc1 = ParentMap.ViewportPointToLocation(new Point(dx, dy));
var loc2 = ParentMap.ViewportPointToLocation(new Point(dx + width, dy));
var loc3 = ParentMap.ViewportPointToLocation(new Point(dx, dy + height));
var loc4 = ParentMap.ViewportPointToLocation(new Point(dx + width, dy + height));
var west = Math.Min(loc1.Longitude, Math.Min(loc2.Longitude, Math.Min(loc3.Longitude, loc4.Longitude)));
var east = Math.Max(loc1.Longitude, Math.Max(loc2.Longitude, Math.Max(loc3.Longitude, loc4.Longitude)));
var south = Math.Min(loc1.Latitude, Math.Min(loc2.Latitude, Math.Min(loc3.Latitude, loc4.Latitude)));
var north = Math.Max(loc1.Latitude, Math.Max(loc2.Latitude, Math.Max(loc3.Latitude, loc4.Latitude)));
var p1 = ParentMap.MapTransform.Transform(new Location(south, west));
var p2 = ParentMap.MapTransform.Transform(new Location(north, east));
width = Math.Round((p2.X - p1.X) * ParentMap.ViewportScale);
height = Math.Round((p2.Y - p1.Y) * ParentMap.ViewportScale);
var image = GetBitmap(west, east, south, north, (int)width, (int)height);
UpdateImage(west, east, south, north, image);
}
UpdateImage(west, east, south, north, (BitmapSource)null);
}
}
private void UpdateImage(double west, double east, double south, double north, BitmapSource image)
protected virtual void UpdateImage(double west, double east, double south, double north, Uri uri)
{
UpdateImage(west, east, south, north, new BitmapImage(uri));
}
protected void UpdateImage(double west, double east, double south, double north, BitmapSource bitmap)
{
currentImageIndex = (currentImageIndex + 1) % 2;
var mapImage = (MapImage)Children[currentImageIndex];
mapImage.Source = null;
mapImage.North = double.NaN; // avoid frequent MapRectangle.UpdateData() calls
mapImage.West = west;
mapImage.East = east;
mapImage.South = south;
mapImage.North = north;
mapImage.Source = bitmap;
if (image != null)
{
mapImage.Source = image;
AddDownloadEventHandlers(image);
}
else
{
BlendImages();
}
ImageUpdated(bitmap);
}
private void BlendImages()
{
#if WINDOWS_RUNTIME
var duration = TimeSpan.Zero; // animation not working in Windows Runtime (?)
#else
var duration = Tile.AnimationDuration;
#endif
var mapImage = (MapImage)Children[currentImageIndex];
var fadeOut = new DoubleAnimation { To = 0d, Duration = duration };
var topImage = (MapImage)Children[currentImageIndex];
var bottomImage = (MapImage)Children[(currentImageIndex + 1) % 2];
if (mapImage.Source != null)
if (topImage.Source != null)
{
mapImage.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation { To = 1d, Duration = duration });
fadeOut.BeginTime = duration;
topImage.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation { To = 1d, Duration = Settings.TileAnimationDuration });
}
mapImage = (MapImage)Children[(currentImageIndex + 1) % 2];
mapImage.BeginAnimation(UIElement.OpacityProperty, fadeOut);
if (bottomImage.Source != null)
{
var fadeOutAnimation = new DoubleAnimation { To = 0d, Duration = Settings.TileAnimationDuration };
if (topImage.Source != null)
{
fadeOutAnimation.BeginTime = Settings.TileAnimationDuration;
}
bottomImage.BeginAnimation(UIElement.OpacityProperty, fadeOutAnimation);
}
updateInProgress = false;
}

View file

@ -72,7 +72,7 @@ namespace MapControl
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement element in InternalChildren)
foreach (UIElement element in Children)
{
var location = GetLocation(element);
@ -92,7 +92,7 @@ namespace MapControl
protected virtual void OnViewportChanged()
{
foreach (UIElement element in InternalChildren)
foreach (UIElement element in Children)
{
var location = GetLocation(element);

View file

@ -0,0 +1,24 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
#if WINDOWS_RUNTIME
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Media;
#endif
namespace MapControl
{
public partial class MapRectangle
{
private void SetRect(Rect rect)
{
((RectangleGeometry)Data).Rect = rect;
RenderTransform = ParentMap.ViewportTransform;
}
}
}

View file

@ -0,0 +1,40 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Windows;
using System.Windows.Media;
namespace MapControl
{
public partial class MapRectangle
{
static MapRectangle()
{
FillTransform.Freeze();
}
private void SetRect(Rect rect)
{
// Apply scaling to the RectangleGeometry to compensate for inaccurate hit testing in WPF.
// See http://stackoverflow.com/a/19335624/1136211
var scale = 1e6 / Math.Min(rect.Width, rect.Height);
rect.X *= scale;
rect.Y *= scale;
rect.Width *= scale;
rect.Height *= scale;
var scaleTransform = new ScaleTransform(1d / scale, 1d / scale);
scaleTransform.Freeze();
var transform = new TransformGroup();
transform.Children.Add(scaleTransform); // revert scaling
transform.Children.Add(ParentMap.ViewportTransform);
((RectangleGeometry)Data).Rect = rect;
RenderTransform = transform;
}
}
}

View file

@ -7,6 +7,7 @@ using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
#else
using System;
using System.Windows;
using System.Windows.Media;
#endif
@ -16,15 +17,15 @@ namespace MapControl
/// <summary>
/// Fills a rectangular area defined by South, North, West and East with a Brush.
/// </summary>
public class MapRectangle : MapPath
public partial class MapRectangle : MapPath
{
public static readonly DependencyProperty SouthProperty = DependencyProperty.Register(
"South", typeof(double), typeof(MapRectangle),
new PropertyMetadata(double.NaN, (o, e) => ((MapRectangle)o).UpdateData()));
public static readonly DependencyProperty NorthProperty = DependencyProperty.Register(
"North", typeof(double), typeof(MapRectangle),
new PropertyMetadata(double.NaN, (o, e) => ((MapRectangle)o).UpdateData()));
/// <summary>
/// Used in derived classes like MapImage.
/// </summary>
protected static readonly MatrixTransform FillTransform = new MatrixTransform
{
Matrix = new Matrix(1d, 0d, 0d, -1d, 0d, 1d)
};
public static readonly DependencyProperty WestProperty = DependencyProperty.Register(
"West", typeof(double), typeof(MapRectangle),
@ -34,24 +35,20 @@ namespace MapControl
"East", typeof(double), typeof(MapRectangle),
new PropertyMetadata(double.NaN, (o, e) => ((MapRectangle)o).UpdateData()));
public static readonly DependencyProperty SouthProperty = DependencyProperty.Register(
"South", typeof(double), typeof(MapRectangle),
new PropertyMetadata(double.NaN, (o, e) => ((MapRectangle)o).UpdateData()));
public static readonly DependencyProperty NorthProperty = DependencyProperty.Register(
"North", typeof(double), typeof(MapRectangle),
new PropertyMetadata(double.NaN, (o, e) => ((MapRectangle)o).UpdateData()));
public MapRectangle()
{
Data = new RectangleGeometry();
StrokeThickness = 0d;
}
public double South
{
get { return (double)GetValue(SouthProperty); }
set { SetValue(SouthProperty, value); }
}
public double North
{
get { return (double)GetValue(NorthProperty); }
set { SetValue(NorthProperty, value); }
}
public double West
{
get { return (double)GetValue(WestProperty); }
@ -64,6 +61,18 @@ namespace MapControl
set { SetValue(EastProperty, value); }
}
public double South
{
get { return (double)GetValue(SouthProperty); }
set { SetValue(SouthProperty, value); }
}
public double North
{
get { return (double)GetValue(NorthProperty); }
set { SetValue(NorthProperty, value); }
}
protected override void UpdateData()
{
var geometry = (RectangleGeometry)Data;
@ -73,25 +82,10 @@ namespace MapControl
!double.IsNaN(West) && !double.IsNaN(East) &&
South < North && West < East)
{
// Create a scaled RectangleGeometry due to inaccurate hit testing in WPF.
// See http://stackoverflow.com/a/19335624/1136211
const double scale = 1e6;
var p1 = ParentMap.MapTransform.Transform(new Location(South, West));
var p2 = ParentMap.MapTransform.Transform(new Location(North, East));
geometry.Rect = new Rect(p1.X * scale, p1.Y * scale, (p2.X - p1.X) * scale, (p2.Y - p1.Y) * scale);
var scaleTransform = new ScaleTransform // revert scaling
{
ScaleX = 1d / scale,
ScaleY = 1d / scale
};
scaleTransform.Freeze();
var transform = new TransformGroup();
transform.Children.Add(scaleTransform);
transform.Children.Add(ParentMap.ViewportTransform);
RenderTransform = transform;
SetRect(new Rect(p1.X, p1.Y, p2.X - p1.X, p2.Y - p1.Y));
}
else
{

View file

@ -19,17 +19,11 @@ namespace MapControl
/// </summary>
public class PanelBase : Panel
{
#if WINDOWS_RUNTIME || SILVERLIGHT
protected internal UIElementCollection InternalChildren
{
get { return Children; }
}
#endif
protected override Size MeasureOverride(Size availableSize)
{
availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
foreach (UIElement element in InternalChildren)
foreach (UIElement element in Children)
{
element.Measure(availableSize);
}
@ -39,7 +33,7 @@ namespace MapControl
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement child in InternalChildren)
foreach (UIElement child in Children)
{
child.Arrange(new Rect(new Point(), finalSize));
}

View file

@ -2,7 +2,10 @@
using System.Runtime.InteropServices;
using System.Windows;
#if SILVERLIGHT
#if WINDOWS_PHONE
[assembly: AssemblyTitle("XAML Map Control (Windows Phone Silverlight)")]
[assembly: AssemblyDescription("XAML Map Control Library for Windows Phone Silverlight")]
#elif SILVERLIGHT
[assembly: AssemblyTitle("XAML Map Control (Silverlight)")]
[assembly: AssemblyDescription("XAML Map Control Library for Silverlight")]
#else
@ -14,8 +17,8 @@ using System.Windows;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © 2014 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("2.2.0")]
[assembly: AssemblyFileVersion("2.2.0")]
[assembly: AssemblyVersion("2.3.0")]
[assembly: AssemblyFileVersion("2.3.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

32
MapControl/Settings.cs Normal file
View file

@ -0,0 +1,32 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © 2014 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if WINDOWS_RUNTIME
using Windows.UI.Xaml.Media.Animation;
#else
using System.Windows.Media.Animation;
#endif
namespace MapControl
{
/// <summary>
/// Stores global static properties that control the behaviour of the map control.
/// </summary>
public static class Settings
{
public static TimeSpan TileUpdateInterval { get; set; }
public static TimeSpan TileAnimationDuration { get; set; }
public static TimeSpan MapAnimationDuration { get; set; }
public static EasingFunctionBase MapAnimationEasingFunction { get; set; }
static Settings()
{
TileUpdateInterval = TimeSpan.FromSeconds(0.5);
TileAnimationDuration = TimeSpan.FromSeconds(0.3);
MapAnimationDuration = TimeSpan.FromSeconds(0.3);
MapAnimationEasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut };
}
}
}

View file

@ -40,6 +40,7 @@
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="3"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Template">
<Setter.Value>

View file

@ -35,7 +35,8 @@ namespace MapControl
}
else
{
Image.BeginAnimation(Image.OpacityProperty, new DoubleAnimation { To = 1d, Duration = AnimationDuration });
Image.BeginAnimation(Image.OpacityProperty,
new DoubleAnimation { To = 1d, Duration = Settings.TileAnimationDuration });
}
}
else
@ -55,7 +56,8 @@ namespace MapControl
bitmap.ImageOpened -= BitmapImageOpened;
bitmap.ImageFailed -= BitmapImageFailed;
Image.BeginAnimation(Image.OpacityProperty, new DoubleAnimation { To = 1d, Duration = AnimationDuration });
Image.BeginAnimation(Image.OpacityProperty,
new DoubleAnimation { To = 1d, Duration = Settings.TileAnimationDuration });
}
private void BitmapImageFailed(object sender, ExceptionRoutedEventArgs e)

View file

@ -27,7 +27,8 @@ namespace MapControl
}
else
{
Image.BeginAnimation(Image.OpacityProperty, new DoubleAnimation(1d, AnimationDuration));
Image.BeginAnimation(Image.OpacityProperty,
new DoubleAnimation(1d, Settings.TileAnimationDuration));
}
}
else
@ -47,7 +48,8 @@ namespace MapControl
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
Image.BeginAnimation(Image.OpacityProperty, new DoubleAnimation(1d, AnimationDuration));
Image.BeginAnimation(Image.OpacityProperty,
new DoubleAnimation(1d, Settings.TileAnimationDuration));
}
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)

View file

@ -13,8 +13,6 @@ namespace MapControl
{
public partial class Tile
{
public static TimeSpan AnimationDuration = TimeSpan.FromSeconds(0.5);
public readonly int ZoomLevel;
public readonly int X;
public readonly int Y;

View file

@ -13,10 +13,8 @@ namespace MapControl
{
internal partial class TileContainer
{
private Matrix GetTileIndexMatrix(int numTiles)
private Matrix GetTileIndexMatrix(double scale)
{
var scale = (double)numTiles / 360d;
return ViewportTransform.Matrix
.Invert() // view to map coordinates
.Translate(180d, -180d)

View file

@ -9,9 +9,8 @@ namespace MapControl
{
internal partial class TileContainer
{
private Matrix GetTileIndexMatrix(int numTiles)
private Matrix GetTileIndexMatrix(double scale)
{
var scale = (double)numTiles / 360d;
var transform = ViewportTransform.Matrix;
transform.Invert(); // view to map coordinates
transform.Translate(180d, -180d);

View file

@ -19,11 +19,9 @@ namespace MapControl
{
internal partial class TileContainer : PanelBase
{
// relative scaled tile size ranges from 0.75 to 1.5 (192 to 384 pixels)
// relative size of scaled tile ranges from 0.75 to 1.5 (192 to 384 pixels)
private static double zoomLevelSwitchDelta = -Math.Log(0.75, 2d);
public static TimeSpan UpdateInterval = TimeSpan.FromSeconds(0.5);
private readonly DispatcherTimer updateTimer;
private Size viewportSize;
private Point viewportOrigin;
@ -38,26 +36,26 @@ namespace MapControl
public TileContainer()
{
RenderTransform = new MatrixTransform();
updateTimer = new DispatcherTimer { Interval = UpdateInterval };
updateTimer = new DispatcherTimer { Interval = Settings.TileUpdateInterval };
updateTimer.Tick += UpdateTiles;
}
public IEnumerable<TileLayer> TileLayers
{
get { return InternalChildren.Cast<TileLayer>(); }
get { return Children.Cast<TileLayer>(); }
}
public void AddTileLayers(int index, IEnumerable<TileLayer> tileLayers)
{
foreach (var tileLayer in tileLayers)
{
if (index < InternalChildren.Count)
if (index < Children.Count)
{
InternalChildren.Insert(index, tileLayer);
Children.Insert(index, tileLayer);
}
else
{
InternalChildren.Add(tileLayer);
Children.Add(tileLayer);
}
index++;
@ -69,19 +67,19 @@ namespace MapControl
{
while (count-- > 0)
{
((TileLayer)InternalChildren[index]).ClearTiles();
InternalChildren.RemoveAt(index);
((TileLayer)Children[index]).ClearTiles();
Children.RemoveAt(index);
}
}
public void ClearTileLayers()
{
foreach (TileLayer tileLayer in InternalChildren)
foreach (TileLayer tileLayer in Children)
{
tileLayer.ClearTiles();
}
InternalChildren.Clear();
Children.Clear();
}
public double SetViewportTransform(double mapZoomLevel, double mapRotation, Point mapOrigin, Point vpOrigin, Size vpSize)
@ -127,7 +125,8 @@ namespace MapControl
updateTimer.Stop();
var zoom = (int)Math.Floor(zoomLevel + zoomLevelSwitchDelta);
var transform = GetTileIndexMatrix(1 << zoom);
var scale = (double)(1 << zoom) / 360d;
var transform = GetTileIndexMatrix(scale);
// tile indices of visible rectangle
var p1 = transform.Transform(new Point(0d, 0d));
@ -149,7 +148,7 @@ namespace MapControl
UpdateRenderTransform();
foreach (TileLayer tileLayer in InternalChildren)
foreach (TileLayer tileLayer in Children)
{
tileLayer.UpdateTiles(tileZoomLevel, tileGrid);
}

View file

@ -13,9 +13,9 @@ namespace MapControl
/// <summary>
/// Loads map tile images.
/// </summary>
internal class TileImageLoader
internal class TileImageLoader : ITileImageLoader
{
internal void BeginGetTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
public void BeginLoadTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
{
var imageTileSource = tileLayer.TileSource as ImageTileSource;
@ -48,7 +48,7 @@ namespace MapControl
}
}
internal void CancelGetTiles()
public void CancelLoadTiles(TileLayer tileLayer)
{
}
}

View file

@ -6,6 +6,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -20,7 +21,7 @@ namespace MapControl
/// <summary>
/// Loads map tile images and optionally caches them in a System.Runtime.Caching.ObjectCache.
/// </summary>
public class TileImageLoader
public class TileImageLoader : ITileImageLoader
{
/// <summary>
/// Default Name of an ObjectCache instance that is assigned to the Cache property.
@ -33,39 +34,27 @@ namespace MapControl
public static readonly string DefaultCacheDirectory =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl");
/// <summary>
/// Default expiration time span for cached images. Used when no expiration date
/// was transmitted on download. The default value is seven days.
/// </summary>
public static TimeSpan DefaultCacheExpiration { get; set; }
/// <summary>
/// The ObjectCache used to cache tile images. The default is MemoryCache.Default.
/// </summary>
public static ObjectCache Cache { get; set; }
/// <summary>
/// The time interval after which cached images expire. The default value is seven days.
/// When an image is not retrieved from the cache during this interval it is considered as expired
/// and will be removed from the cache, provided that the cache implementation supports expiration.
/// If an image is retrieved from the cache and the CacheUpdateAge time interval has expired,
/// the image is downloaded again and rewritten to the cache with a new expiration time.
/// </summary>
public static TimeSpan CacheExpiration { get; set; }
/// <summary>
/// The time interval after which a cached image is updated and rewritten to the cache.
/// The default value is one day. This time interval should not be greater than the value
/// of the CacheExpiration property.
/// If CacheUpdateAge is less than or equal to TimeSpan.Zero, cached images are never updated.
/// </summary>
public static TimeSpan CacheUpdateAge { get; set; }
static TileImageLoader()
{
DefaultCacheExpiration = TimeSpan.FromDays(7);
Cache = MemoryCache.Default;
CacheExpiration = TimeSpan.FromDays(7);
CacheUpdateAge = TimeSpan.FromDays(1);
}
private readonly ConcurrentQueue<Tile> pendingTiles = new ConcurrentQueue<Tile>();
private int threadCount;
internal void BeginGetTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
public void BeginLoadTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
{
if (tiles.Any())
{
@ -96,7 +85,7 @@ namespace MapControl
}
}
internal void CancelGetTiles()
public void CancelLoadTiles(TileLayer tileLayer)
{
Tile tile;
while (pendingTiles.TryDequeue(out tile)) ; // no Clear method
@ -119,7 +108,7 @@ namespace MapControl
{
pendingTiles.Enqueue(tile); // not yet cached
}
else if (CacheUpdateAge > TimeSpan.Zero && TileCache.CreationTime(buffer) + CacheUpdateAge < DateTime.UtcNow)
else if (TileCache.IsExpired(buffer))
{
dispatcher.Invoke(setImageAction, tile, image); // synchronously before enqueuing
outdatedTiles.Add(tile); // update outdated cache
@ -154,7 +143,6 @@ namespace MapControl
while (pendingTiles.TryDequeue(out tile))
{
byte[] buffer = null;
ImageSource image = null;
if (imageTileSource != null)
@ -167,14 +155,24 @@ namespace MapControl
if (uri != null)
{
if (uri.Scheme == "file") // create from FileStream because creating from URI leaves the file open
if (uri.Scheme == "file") // create from FileStream because creating from Uri leaves the file open
{
image = CreateImage(uri.LocalPath);
}
else
{
buffer = DownloadImage(uri);
DateTime expirationTime;
var buffer = DownloadImage(uri, out expirationTime);
image = CreateImage(buffer);
if (image != null &&
Cache != null &&
!string.IsNullOrWhiteSpace(sourceName) &&
expirationTime > DateTime.UtcNow)
{
Cache.Set(TileCache.Key(sourceName, tile), buffer, new CacheItemPolicy { AbsoluteExpiration = expirationTime });
}
}
}
}
@ -183,11 +181,6 @@ namespace MapControl
{
dispatcher.BeginInvoke(setImageAction, tile, image);
}
if (image != null && buffer != null && Cache != null && !string.IsNullOrWhiteSpace(sourceName))
{
Cache.Set(TileCache.Key(sourceName, tile), buffer, new CacheItemPolicy { SlidingExpiration = CacheExpiration });
}
}
Interlocked.Decrement(ref threadCount);
@ -253,20 +246,33 @@ namespace MapControl
return image;
}
private static byte[] DownloadImage(Uri uri)
private static byte[] DownloadImage(Uri uri, out DateTime expirationTime)
{
expirationTime = DateTime.UtcNow + DefaultCacheExpiration;
byte[] buffer = null;
try
{
var request = (HttpWebRequest)WebRequest.Create(uri);
request.UserAgent = "XAML Map Control";
var request = HttpWebRequest.CreateHttp(uri);
using (var response = (HttpWebResponse)request.GetResponse())
using (var responseStream = response.GetResponseStream())
{
buffer = TileCache.CreateBuffer(responseStream, (int)response.ContentLength);
var expiresHeader = response.Headers["Expires"];
DateTime expires;
if (expiresHeader != null &&
DateTime.TryParse(expiresHeader, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out expires) &&
expirationTime > expires)
{
expirationTime = expires;
}
buffer = TileCache.CreateBuffer(responseStream, (int)response.ContentLength, expirationTime);
}
//Trace.TraceInformation("Downloaded {0}, expires {1}", uri, expirationTime);
}
catch (WebException ex)
{
@ -305,18 +311,16 @@ namespace MapControl
return new MemoryStream(cacheBuffer, imageBufferOffset, cacheBuffer.Length - imageBufferOffset, false);
}
public static DateTime CreationTime(byte[] cacheBuffer)
public static bool IsExpired(byte[] cacheBuffer)
{
return DateTime.FromBinary(BitConverter.ToInt64(cacheBuffer, 0));
return DateTime.FromBinary(BitConverter.ToInt64(cacheBuffer, 0)) < DateTime.UtcNow;
}
public static byte[] CreateBuffer(Stream imageStream, int length)
public static byte[] CreateBuffer(Stream imageStream, int length, DateTime expirationTime)
{
var creationTime = BitConverter.GetBytes(DateTime.UtcNow.ToBinary());
using (var memoryStream = length > 0 ? new MemoryStream(length + imageBufferOffset) : new MemoryStream())
{
memoryStream.Write(creationTime, 0, imageBufferOffset);
memoryStream.Write(BitConverter.GetBytes(expirationTime.ToBinary()), 0, imageBufferOffset);
imageStream.CopyTo(memoryStream);
return length > 0 ? memoryStream.GetBuffer() : memoryStream.ToArray();

View file

@ -20,13 +20,13 @@ namespace MapControl
/// <summary>
/// Loads map tile images.
/// </summary>
public class TileImageLoader
public class TileImageLoader : ITileImageLoader
{
public static IObjectCache Cache { get; set; }
private HttpClient httpClient;
internal void BeginGetTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
public void BeginLoadTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
{
var imageTileSource = tileLayer.TileSource as ImageTileSource;
@ -69,7 +69,7 @@ namespace MapControl
}
}
internal void CancelGetTiles()
public void CancelLoadTiles(TileLayer tileLayer)
{
}

View file

@ -7,16 +7,24 @@ using System.Collections.Generic;
using System.Linq;
#if WINDOWS_RUNTIME
using Windows.Foundation;
using Windows.UI.Xaml.Documents;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Documents;
using System.Windows.Markup;
using System.Windows.Media;
#endif
namespace MapControl
{
public interface ITileImageLoader
{
void BeginLoadTiles(TileLayer tileLayer, IEnumerable<Tile> tiles);
void CancelLoadTiles(TileLayer tileLayer);
}
/// <summary>
/// Fills a rectangular area with map tiles from a TileSource.
/// </summary>
@ -34,29 +42,35 @@ namespace MapControl
return new TileLayer
{
SourceName = "OpenStreetMap",
Description = "© {y} OpenStreetMap Contributors, CC-BY-SA",
TileSource = new TileSource("http://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png")
Description="© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)",
TileSource = new TileSource { UriFormat = "http://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png" }
};
}
}
private readonly TileImageLoader tileImageLoader = new TileImageLoader();
private string description = string.Empty;
private readonly ITileImageLoader tileImageLoader;
private TileSource tileSource;
private List<Tile> tiles = new List<Tile>();
private Int32Rect grid;
private int zoomLevel;
private Int32Rect grid;
public TileLayer()
: this(new TileImageLoader())
{
}
public TileLayer(ITileImageLoader tileImageLoader)
{
this.tileImageLoader = tileImageLoader;
MinZoomLevel = 0;
MaxZoomLevel = 18;
MaxParallelDownloads = 8;
MaxParallelDownloads = 4;
LoadLowerZoomLevels = true;
AnimateTileOpacity = true;
}
public string SourceName { get; set; }
public string Description { get; set; }
public int MinZoomLevel { get; set; }
public int MaxZoomLevel { get; set; }
public int MaxParallelDownloads { get; set; }
@ -64,10 +78,14 @@ namespace MapControl
public bool AnimateTileOpacity { get; set; }
public Brush Foreground { get; set; }
public string Description
/// <summary>
/// In case the Description text contains copyright links in markdown syntax [text](url),
/// the DescriptionInlines property may be used to create a collection of Run and Hyperlink
/// inlines to be displayed in e.g. a TextBlock or a Silverlight RichTextBlock.
/// </summary>
public ICollection<Inline> DescriptionInlines
{
get { return description; }
set { description = value.Replace("{y}", DateTime.Now.Year.ToString()); }
get { return Description.ToInlines(); }
}
public TileSource TileSource
@ -79,42 +97,43 @@ namespace MapControl
if (grid.Width > 0 && grid.Height > 0)
{
tileImageLoader.CancelGetTiles();
tiles.Clear();
if (tileSource != null)
{
SelectTiles();
RenderTiles();
tileImageLoader.BeginGetTiles(this, tiles.Where(t => !t.HasImageSource));
}
else
{
RenderTiles();
}
ClearTiles();
UpdateTiles();
}
}
}
internal void UpdateTiles(int zoomLevel, Int32Rect grid)
{
this.grid = grid;
this.zoomLevel = zoomLevel;
if (tileSource != null)
{
tileImageLoader.CancelGetTiles();
SelectTiles();
RenderTiles();
tileImageLoader.BeginGetTiles(this, tiles.Where(t => !t.HasImageSource));
}
}
internal void ClearTiles()
{
tileImageLoader.CancelGetTiles();
tileImageLoader.CancelLoadTiles(this);
tiles.Clear();
RenderTiles();
Children.Clear();
}
internal void UpdateTiles(int zoomLevel, Int32Rect grid)
{
this.zoomLevel = zoomLevel;
this.grid = grid;
UpdateTiles();
}
private void UpdateTiles()
{
if (tileSource != null)
{
tileImageLoader.CancelLoadTiles(this);
SelectTiles();
Children.Clear();
foreach (var tile in tiles)
{
Children.Add(tile.Image);
}
tileImageLoader.BeginLoadTiles(this, tiles.Where(t => !t.HasImageSource));
}
}
private void SelectTiles()
@ -122,7 +141,9 @@ namespace MapControl
var maxZoomLevel = Math.Min(zoomLevel, MaxZoomLevel);
var minZoomLevel = maxZoomLevel;
if (LoadLowerZoomLevels && Parent is TileContainer && ((TileContainer)Parent).TileLayers.FirstOrDefault() == this)
if (LoadLowerZoomLevels &&
Parent is TileContainer &&
((TileContainer)Parent).TileLayers.FirstOrDefault() == this)
{
minZoomLevel = MinZoomLevel;
}
@ -165,16 +186,6 @@ namespace MapControl
tiles = newTiles;
}
private void RenderTiles()
{
InternalChildren.Clear();
foreach (var tile in tiles)
{
InternalChildren.Add(tile.Image);
}
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var tile in tiles)

View file

@ -4,7 +4,6 @@
using System;
using System.Globalization;
using System.Text;
#if WINDOWS_RUNTIME
using Windows.Foundation;
#else
@ -28,12 +27,11 @@ namespace MapControl
{
}
public TileSource(string uriFormat)
: this()
protected TileSource(string uriFormat)
{
UriFormat = uriFormat;
this.uriFormat = uriFormat;
}
public string UriFormat
{
get { return uriFormat; }
@ -147,16 +145,16 @@ namespace MapControl
return null;
}
var key = new StringBuilder { Length = zoomLevel };
var quadkey = new char[zoomLevel];
for (var z = zoomLevel - 1; z >= 0; z--, x /= 2, y /= 2)
{
key[z] = (char)('0' + 2 * (y % 2) + (x % 2));
quadkey[z] = (char)('0' + 2 * (y % 2) + (x % 2));
}
return new Uri(uriFormat.
Replace("{i}", key.ToString(key.Length - 1, 1)).
Replace("{q}", key.ToString()));
Replace("{i}", new string(quadkey[zoomLevel - 1], 1)).
Replace("{q}", new string(quadkey)));
}
private Uri GetBoundingBoxUri(int x, int y, int zoomLevel)

View file

@ -17,7 +17,7 @@ namespace MapControl
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return new TileSource(value as string);
return new TileSource { UriFormat = value as string };
}
}

View file

@ -36,12 +36,21 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\BingMapsTileLayer.cs">
<Link>BingMapsTileLayer.cs</Link>
</Compile>
<Compile Include="..\BingMapsTileSource.cs">
<Link>BingMapsTileSource.cs</Link>
</Compile>
<Compile Include="..\Extensions.Silverlight.WinRT.cs">
<Link>Extensions.Silverlight.WinRT.cs</Link>
</Compile>
<Compile Include="..\Extensions.WinRT.cs">
<Link>Extensions.WinRT.cs</Link>
</Compile>
<Compile Include="..\HyperlinkText.cs">
<Link>HyperlinkText.cs</Link>
</Compile>
<Compile Include="..\ImageFileCache.WinRT.cs">
<Link>ImageFileCache.WinRT.cs</Link>
</Compile>
@ -120,6 +129,9 @@
<Compile Include="..\MapRectangle.cs">
<Link>MapRectangle.cs</Link>
</Compile>
<Compile Include="..\MapRectangle.Silverlight.WinRT.cs">
<Link>MapRectangle.Silverlight.WinRT.cs</Link>
</Compile>
<Compile Include="..\MapTransform.cs">
<Link>MapTransform.cs</Link>
</Compile>
@ -132,6 +144,9 @@
<Compile Include="..\Pushpin.Silverlight.WinRT.cs">
<Link>Pushpin.Silverlight.WinRT.cs</Link>
</Compile>
<Compile Include="..\Settings.cs">
<Link>Settings.cs</Link>
</Compile>
<Compile Include="..\Tile.cs">
<Link>Tile.cs</Link>
</Compile>
@ -163,16 +178,17 @@
<Link>MapControl.snk</Link>
</None>
</ItemGroup>
<ItemGroup>
<Page Include="Themes\Generic.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<TargetPlatform Include="Windows, Version=8.1" />
<TargetPlatform Include="WindowsPhoneApp, Version=8.1" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Page Include="Themes\Generic.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '12.0' ">
<VisualStudioVersion>12.0</VisualStudioVersion>
</PropertyGroup>

View file

@ -7,8 +7,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © 2014 Clemens Fischer")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("2.2.0")]
[assembly: AssemblyFileVersion("2.2.0")]
[assembly: AssemblyVersion("2.3.0")]
[assembly: AssemblyFileVersion("2.3.0")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]

View file

@ -40,6 +40,7 @@
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="3"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Template">
<Setter.Value>