Version 1.8.0: Fixed MapImage, added ImageTileSource to Silverlight and WinRT versions, modified TileImageLoader.

This commit is contained in:
ClemensF 2013-11-12 21:14:53 +01:00
parent 8eedb82a9d
commit 9f4ab0f3e3
29 changed files with 331 additions and 285 deletions

View file

@ -8,8 +8,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")] [assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.7.0")] [assembly: AssemblyVersion("1.8.0")]
[assembly: AssemblyFileVersion("1.7.0")] [assembly: AssemblyFileVersion("1.8.0")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]

View file

@ -16,12 +16,22 @@ namespace Caching
{ {
/// <summary> /// <summary>
/// ObjectCache implementation based on local image files. /// ObjectCache implementation based on local image files.
/// The only valid data type for cached values is a byte[], which contains /// The only valid data type for cached values is a byte array containing an
/// an 8-byte binary UTC time (as created by DateTime.ToBinary), followed /// 8-byte timestamp followed by a PNG, JPEG, BMP, GIF, TIFF or WMP image buffer.
/// by a PNG, JPEG, BMP, GIF, TIFF or WMP image buffer.
/// </summary> /// </summary>
public class ImageFileCache : ObjectCache public class ImageFileCache : ObjectCache
{ {
private static readonly Tuple<string, byte[]>[] imageFileTypes = new Tuple<string, byte[]>[]
{
new Tuple<string, byte[]>(".png", new byte[] { 0x89, 0x50, 0x4E, 0x47, 0xD, 0xA, 0x1A, 0xA }),
new Tuple<string, byte[]>(".jpg", new byte[] { 0xFF, 0xD8, 0xFF, 0xE0, 0, 0x10, 0x4A, 0x46, 0x49, 0x46, 0 }),
new Tuple<string, byte[]>(".bmp", new byte[] { 0x42, 0x4D }),
new Tuple<string, byte[]>(".gif", new byte[] { 0x47, 0x49, 0x46 }),
new Tuple<string, byte[]>(".tif", new byte[] { 0x49, 0x49, 42, 0 }),
new Tuple<string, byte[]>(".tif", new byte[] { 0x4D, 0x4D, 0, 42 }),
new Tuple<string, byte[]>(".wdp", new byte[] { 0x49, 0x49, 0xBC }),
};
private static readonly FileSystemAccessRule fullControlRule = new FileSystemAccessRule( private static readonly FileSystemAccessRule fullControlRule = new FileSystemAccessRule(
new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),
FileSystemRights.FullControl, AccessControlType.Allow); FileSystemRights.FullControl, AccessControlType.Allow);
@ -169,36 +179,31 @@ namespace Caching
var buffer = value as byte[]; var buffer = value as byte[];
if (buffer == null) if (buffer == null || buffer.Length <= 8)
{ {
throw new NotSupportedException("The parameter value must be a byte[]."); throw new NotSupportedException("The parameter value must be a byte[] containing at least 9 bytes.");
} }
MemoryCache.Default.Set(key, buffer, policy); MemoryCache.Default.Set(key, buffer, policy);
var extension = GetFileExtension(buffer); var path = GetPath(key) + GetFileExtension(buffer);
if (extension != null) try
{ {
var path = GetPath(key) + extension; Directory.CreateDirectory(Path.GetDirectoryName(path));
try using (var fileStream = new FileStream(path, FileMode.Create))
{ {
Directory.CreateDirectory(Path.GetDirectoryName(path)); fileStream.Write(buffer, 8, buffer.Length - 8);
using (var fileStream = new FileStream(path, FileMode.Create))
{
fileStream.Write(buffer, 8, buffer.Length - 8);
}
var fileSecurity = File.GetAccessControl(path);
fileSecurity.AddAccessRule(fullControlRule);
File.SetAccessControl(path, fileSecurity);
}
catch (Exception ex)
{
Trace.TraceWarning("ImageFileCache: Writing file {0} failed: {1}", path, ex.Message);
} }
var fileSecurity = File.GetAccessControl(path);
fileSecurity.AddAccessRule(fullControlRule);
File.SetAccessControl(path, fileSecurity);
}
catch (Exception ex)
{
Trace.TraceWarning("ImageFileCache: Writing file {0} failed: {1}", path, ex.Message);
} }
} }
@ -274,42 +279,24 @@ namespace Caching
return null; return null;
} }
private static readonly Tuple<string, byte[]>[] fileTypes = new Tuple<string, byte[]>[]
{
new Tuple<string, byte[]>(".png", new byte[] { 0x89, 0x50, 0x4E, 0x47, 0xD, 0xA, 0x1A, 0xA }),
new Tuple<string, byte[]>(".jpg", new byte[] { 0xFF, 0xD8, 0xFF, 0xE0, 0, 0x10, 0x4A, 0x46, 0x49, 0x46, 0 }),
new Tuple<string, byte[]>(".bmp", new byte[] { 0x42, 0x4D }),
new Tuple<string, byte[]>(".gif", new byte[] { 0x47, 0x49, 0x46 }),
new Tuple<string, byte[]>(".tif", new byte[] { 0x49, 0x49, 42, 0 }),
new Tuple<string, byte[]>(".tif", new byte[] { 0x4D, 0x4D, 0, 42 }),
new Tuple<string, byte[]>(".wdp", new byte[] { 0x49, 0x49, 0xBC }),
};
private static string GetFileExtension(byte[] buffer) private static string GetFileExtension(byte[] buffer)
{ {
string extension = null; var fileType = imageFileTypes.FirstOrDefault(t =>
var creationTime = DateTime.FromBinary(BitConverter.ToInt64(buffer, 0));
if (creationTime.Kind == DateTimeKind.Utc && creationTime <= DateTime.UtcNow)
{ {
Func<Tuple<string, byte[]>, bool> match = int i = 0;
t =>
if (t.Item2.Length <= buffer.Length - 8)
{
while (i < t.Item2.Length && t.Item2[i] == buffer[i + 8])
{ {
int i = 0; i++;
if (t.Item2.Length + 8 <= buffer.Length) }
{ }
while (i < t.Item2.Length && t.Item2[i] == buffer[i + 8])
{
i++;
}
}
return i == t.Item2.Length;
};
extension = fileTypes.Where(match).Select(t => t.Item1).FirstOrDefault(); return i == t.Item2.Length;
} });
return extension; return fileType != null ? fileType.Item1 : ".bin";
} }
} }
} }

View file

@ -8,8 +8,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")] [assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.7.0")] [assembly: AssemblyVersion("1.8.0")]
[assembly: AssemblyFileVersion("1.7.0")] [assembly: AssemblyFileVersion("1.8.0")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]

View file

@ -1,22 +0,0 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © Clemens Fischer 2012-2013
// Licensed under the Microsoft Public License (Ms-PL)
#if NETFX_CORE
using Windows.UI.Xaml;
#else
using System.Windows;
#endif
namespace MapControl
{
internal static class Freezable
{
/// <summary>
/// Provides WPF compatibility.
/// </summary>
public static void Freeze(this DependencyObject obj)
{
}
}
}

View file

@ -0,0 +1,14 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © Clemens Fischer 2012-2013
// Licensed under the Microsoft Public License (Ms-PL)
namespace MapControl
{
public partial class ImageTileSource
{
public virtual bool CanLoadAsync
{
get { return false; }
}
}
}

View file

@ -2,28 +2,30 @@
// Copyright © Clemens Fischer 2012-2013 // Copyright © Clemens Fischer 2012-2013
// Licensed under the Microsoft Public License (Ms-PL) // Licensed under the Microsoft Public License (Ms-PL)
#if NETFX_CORE
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
#else
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
#endif
namespace MapControl namespace MapControl
{ {
/// <summary> /// <summary>
/// Provides the image of a map tile. ImageTileSource bypasses download and /// Provides the image of a map tile. ImageTileSource bypasses downloading and
/// cache processing in TileImageLoader. By overriding the LoadImage method, /// optional caching in TileImageLoader. By overriding the LoadImage method,
/// an application can provide tile images from an arbitrary source. /// an application can provide tile images from an arbitrary source.
/// If the CanLoadAsync property is true, the LoadImage method will be called /// WPF only: If the CanLoadAsync property is true, LoadImage will be called
/// from a separate, non-UI thread and must hence return a frozen ImageSource. /// from a separate, non-UI thread and must hence return a frozen ImageSource.
/// </summary> /// </summary>
public class ImageTileSource : TileSource public partial class ImageTileSource : TileSource
{ {
public virtual bool CanLoadAsync
{
get { return false; }
}
public virtual ImageSource LoadImage(int x, int y, int zoomLevel) public virtual ImageSource LoadImage(int x, int y, int zoomLevel)
{ {
return new BitmapImage(GetUri(x, y, zoomLevel)); var uri = GetUri(x, y, zoomLevel);
return uri != null ? new BitmapImage(uri) : null;
} }
} }
} }

View file

@ -70,7 +70,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="AnimationEx.Silverlight.cs" /> <Compile Include="AnimationEx.Silverlight.cs" />
<Compile Include="Freezable.cs" /> <Compile Include="ImageTileSource.cs" />
<Compile Include="Int32Rect.cs" /> <Compile Include="Int32Rect.cs" />
<Compile Include="Location.cs" /> <Compile Include="Location.cs" />
<Compile Include="LocationCollection.cs" /> <Compile Include="LocationCollection.cs" />

View file

@ -53,6 +53,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="GlyphRunText.cs" /> <Compile Include="GlyphRunText.cs" />
<Compile Include="ImageTileSource.cs" /> <Compile Include="ImageTileSource.cs" />
<Compile Include="ImageTileSource.WPF.cs" />
<Compile Include="Location.cs" /> <Compile Include="Location.cs" />
<Compile Include="LocationCollection.cs" /> <Compile Include="LocationCollection.cs" />
<Compile Include="LocationCollectionConverter.cs" /> <Compile Include="LocationCollectionConverter.cs" />
@ -63,6 +64,7 @@
<Compile Include="MapGraticule.cs" /> <Compile Include="MapGraticule.cs" />
<Compile Include="MapGraticule.WPF.cs" /> <Compile Include="MapGraticule.WPF.cs" />
<Compile Include="MapImage.cs" /> <Compile Include="MapImage.cs" />
<Compile Include="MapImage.WPF.cs" />
<Compile Include="MapImageLayer.cs" /> <Compile Include="MapImageLayer.cs" />
<Compile Include="MapItem.WPF.cs" /> <Compile Include="MapItem.WPF.cs" />
<Compile Include="MapItemsControl.WPF.cs" /> <Compile Include="MapItemsControl.WPF.cs" />
@ -74,6 +76,7 @@
<Compile Include="MapPolyline.cs" /> <Compile Include="MapPolyline.cs" />
<Compile Include="MapPolyline.WPF.cs" /> <Compile Include="MapPolyline.WPF.cs" />
<Compile Include="MapRectangle.cs" /> <Compile Include="MapRectangle.cs" />
<Compile Include="MapRectangle.WPF.cs" />
<Compile Include="MapScale.cs" /> <Compile Include="MapScale.cs" />
<Compile Include="MapTransform.cs" /> <Compile Include="MapTransform.cs" />
<Compile Include="MercatorTransform.cs" /> <Compile Include="MercatorTransform.cs" />

View file

@ -0,0 +1,14 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © Clemens Fischer 2012-2013
// Licensed under the Microsoft Public License (Ms-PL)
namespace MapControl
{
public partial class MapImage
{
static MapImage()
{
imageTransform.Freeze();
}
}
}

View file

@ -15,18 +15,13 @@ namespace MapControl
/// <summary> /// <summary>
/// Fills a rectangular area with an ImageBrush from the Source property. /// Fills a rectangular area with an ImageBrush from the Source property.
/// </summary> /// </summary>
public class MapImage : MapRectangle public partial class MapImage : MapRectangle
{ {
private static readonly MatrixTransform imageTransform = new MatrixTransform private static readonly MatrixTransform imageTransform = new MatrixTransform
{ {
Matrix = new Matrix(1d, 0d, 0d, -1d, 0d, 1d) Matrix = new Matrix(1d, 0d, 0d, -1d, 0d, 1d)
}; };
static MapImage()
{
imageTransform.Freeze();
}
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register( public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
"Source", typeof(ImageSource), typeof(MapImage), "Source", typeof(ImageSource), typeof(MapImage),
new PropertyMetadata(null, (o, e) => ((MapImage)o).SourceChanged((ImageSource)e.NewValue))); new PropertyMetadata(null, (o, e) => ((MapImage)o).SourceChanged((ImageSource)e.NewValue)));
@ -45,7 +40,6 @@ namespace MapControl
RelativeTransform = imageTransform RelativeTransform = imageTransform
}; };
imageBrush.Freeze();
Fill = imageBrush; Fill = imageBrush;
} }
} }

View file

@ -119,7 +119,6 @@ namespace MapControl
try try
{ {
var bitmap = new BitmapImage();
var request = (HttpWebRequest)WebRequest.Create(uri); var request = (HttpWebRequest)WebRequest.Create(uri);
request.UserAgent = "XAML Map Control"; request.UserAgent = "XAML Map Control";
@ -128,15 +127,8 @@ namespace MapControl
using (var memoryStream = new MemoryStream()) using (var memoryStream = new MemoryStream())
{ {
responseStream.CopyTo(memoryStream); responseStream.CopyTo(memoryStream);
image = BitmapFrame.Create(memoryStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = memoryStream;
bitmap.EndInit();
bitmap.Freeze();
} }
image = bitmap;
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -0,0 +1,14 @@
// XAML Map Control - http://xamlmapcontrol.codeplex.com/
// Copyright © Clemens Fischer 2012-2013
// Licensed under the Microsoft Public License (Ms-PL)
namespace MapControl
{
public partial class MapRectangle
{
static MapRectangle()
{
geometryScaleTransform.Freeze();
}
}
}

View file

@ -16,7 +16,7 @@ namespace MapControl
/// <summary> /// <summary>
/// Fills a rectangular area defined by South, North, West and East with a Brush. /// Fills a rectangular area defined by South, North, West and East with a Brush.
/// </summary> /// </summary>
public class MapRectangle : MapPath public partial class MapRectangle : MapPath
{ {
private const double geometryScale = 1e6; private const double geometryScale = 1e6;
@ -26,11 +26,6 @@ namespace MapControl
ScaleY = 1d / geometryScale ScaleY = 1d / geometryScale
}; };
static MapRectangle()
{
geometryScaleTransform.Freeze();
}
public static readonly DependencyProperty SouthProperty = DependencyProperty.Register( public static readonly DependencyProperty SouthProperty = DependencyProperty.Register(
"South", typeof(double), typeof(MapRectangle), "South", typeof(double), typeof(MapRectangle),
new PropertyMetadata(double.NaN, (o, e) => ((MapRectangle)o).UpdateData())); new PropertyMetadata(double.NaN, (o, e) => ((MapRectangle)o).UpdateData()));

View file

@ -15,8 +15,8 @@ using System.Windows;
[assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")] [assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.7.0")] [assembly: AssemblyVersion("1.8.0")]
[assembly: AssemblyFileVersion("1.7.0")] [assembly: AssemblyFileVersion("1.8.0")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]

View file

@ -33,12 +33,12 @@ namespace MapControl
{ {
if (animateOpacity) if (animateOpacity)
{ {
var bitmapImage = image as BitmapImage; var bitmap = image as BitmapImage;
if (bitmapImage != null) if (bitmap != null)
{ {
bitmapImage.ImageOpened += BitmapImageOpened; bitmap.ImageOpened += BitmapImageOpened;
bitmapImage.ImageFailed += BitmapImageFailed; bitmap.ImageFailed += BitmapImageFailed;
} }
else else
{ {
@ -52,20 +52,26 @@ namespace MapControl
} }
Image.Source = image; Image.Source = image;
HasImage = true; HasImageSource = true;
} }
private void BitmapImageOpened(object sender, RoutedEventArgs e) private void BitmapImageOpened(object sender, RoutedEventArgs e)
{ {
((BitmapImage)sender).ImageOpened -= BitmapImageOpened; var bitmap = (BitmapImage)sender;
((BitmapImage)sender).ImageFailed -= BitmapImageFailed;
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 = AnimationDuration });
} }
private void BitmapImageFailed(object sender, ExceptionRoutedEventArgs e) private void BitmapImageFailed(object sender, ExceptionRoutedEventArgs e)
{ {
((BitmapImage)sender).ImageOpened -= BitmapImageOpened; var bitmap = (BitmapImage)sender;
((BitmapImage)sender).ImageFailed -= BitmapImageFailed;
bitmap.ImageOpened -= BitmapImageOpened;
bitmap.ImageFailed -= BitmapImageFailed;
Image.Source = null; Image.Source = null;
} }
} }

View file

@ -24,12 +24,12 @@ namespace MapControl
{ {
if (animateOpacity) if (animateOpacity)
{ {
var bitmapImage = image as BitmapImage; var bitmap = image as BitmapSource;
if (bitmapImage != null && bitmapImage.IsDownloading) if (bitmap != null && !bitmap.IsFrozen && bitmap.IsDownloading)
{ {
bitmapImage.DownloadCompleted += BitmapDownloadCompleted; bitmap.DownloadCompleted += BitmapDownloadCompleted;
bitmapImage.DownloadFailed += BitmapDownloadFailed; bitmap.DownloadFailed += BitmapDownloadFailed;
} }
else else
{ {
@ -43,20 +43,26 @@ namespace MapControl
} }
Brush.ImageSource = image; Brush.ImageSource = image;
HasImage = true; HasImageSource = true;
} }
private void BitmapDownloadCompleted(object sender, EventArgs e) private void BitmapDownloadCompleted(object sender, EventArgs e)
{ {
((BitmapImage)sender).DownloadCompleted -= BitmapDownloadCompleted; var bitmap = (BitmapSource)sender;
((BitmapImage)sender).DownloadFailed -= BitmapDownloadFailed;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
Brush.BeginAnimation(ImageBrush.OpacityProperty, new DoubleAnimation(1d, AnimationDuration)); Brush.BeginAnimation(ImageBrush.OpacityProperty, new DoubleAnimation(1d, AnimationDuration));
} }
private void BitmapDownloadFailed(object sender, ExceptionEventArgs e) private void BitmapDownloadFailed(object sender, ExceptionEventArgs e)
{ {
((BitmapImage)sender).DownloadCompleted -= BitmapDownloadCompleted; var bitmap = (BitmapSource)sender;
((BitmapImage)sender).DownloadFailed -= BitmapDownloadFailed;
bitmap.DownloadCompleted -= BitmapDownloadCompleted;
bitmap.DownloadFailed -= BitmapDownloadFailed;
Brush.ImageSource = null; Brush.ImageSource = null;
} }
} }

View file

@ -21,7 +21,7 @@ namespace MapControl
Y = y; Y = y;
} }
public bool HasImage { get; private set; } public bool HasImageSource { get; private set; }
public int XIndex public int XIndex
{ {

View file

@ -2,37 +2,50 @@
// Copyright © Clemens Fischer 2012-2013 // Copyright © Clemens Fischer 2012-2013
// Licensed under the Microsoft Public License (Ms-PL) // Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Diagnostics;
#if NETFX_CORE #if NETFX_CORE
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging; using Windows.UI.Xaml.Media.Imaging;
#else #else
using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
#endif #endif
namespace MapControl namespace MapControl
{ {
/// <summary> /// <summary>
/// Loads map tile images by their URIs. /// Loads map tile images.
/// </summary> /// </summary>
internal class TileImageLoader internal class TileImageLoader
{ {
private readonly TileLayer tileLayer; internal void BeginGetTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
internal TileImageLoader(TileLayer tileLayer)
{ {
this.tileLayer = tileLayer; var imageTileSource = tileLayer.TileSource as ImageTileSource;
}
internal void StartGetTiles(IEnumerable<Tile> tiles)
{
foreach (var tile in tiles) foreach (var tile in tiles)
{ {
var uri = tileLayer.TileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel); try
if (uri != null)
{ {
tile.SetImageSource(new BitmapImage(uri), tileLayer.AnimateTileOpacity); ImageSource image;
if (imageTileSource != null)
{
image = imageTileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel);
}
else
{
var uri = tileLayer.TileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
image = uri != null ? new BitmapImage(uri) : null;
}
tile.SetImageSource(image, tileLayer.AnimateTileOpacity);
}
catch (Exception ex)
{
Debug.WriteLine("Creating tile image failed: {0}", ex.Message);
} }
} }
} }

View file

@ -9,7 +9,6 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Cache;
using System.Runtime.Caching; using System.Runtime.Caching;
using System.Threading; using System.Threading;
using System.Windows.Media; using System.Windows.Media;
@ -19,18 +18,14 @@ using System.Windows.Threading;
namespace MapControl namespace MapControl
{ {
/// <summary> /// <summary>
/// Loads map tile images by their URIs and optionally caches the images in an ObjectCache. /// Loads map tile images and optionally caches them in a System.Runtime.Caching.ObjectCache.
/// </summary> /// </summary>
public class TileImageLoader public class TileImageLoader
{ {
private readonly TileLayer tileLayer;
private readonly ConcurrentQueue<Tile> pendingTiles = new ConcurrentQueue<Tile>();
private int downloadThreadCount;
/// <summary> /// <summary>
/// Default Name of an ObjectCache instance that is assigned to the Cache property. /// Default Name of an ObjectCache instance that is assigned to the Cache property.
/// </summary> /// </summary>
public static readonly string DefaultCacheName = "TileCache"; public const string DefaultCacheName = "TileCache";
/// <summary> /// <summary>
/// Default value for the directory where an ObjectCache instance may save cached data. /// Default value for the directory where an ObjectCache instance may save cached data.
@ -39,44 +34,48 @@ namespace MapControl
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl"); Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl");
/// <summary> /// <summary>
/// The ObjectCache used to cache tile images. /// The ObjectCache used to cache tile images. The default is null.
/// The default is System.Runtime.Caching.MemoryCache.Default.
/// </summary> /// </summary>
public static ObjectCache Cache { get; set; } public static ObjectCache Cache { get; set; }
/// <summary> /// <summary>
/// The time interval after which cached images expire. The default value is 30 days. /// 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 /// When an image is not retrieved from the cache during this interval it is considered as expired
/// as expired and will be removed from the cache. If an image is retrieved from the /// and will be removed from the cache, provided that the cache implementation supports expiration.
/// cache and the CacheUpdateAge time interval has expired, the image is downloaded /// If an image is retrieved from the cache and the CacheUpdateAge time interval has expired,
/// again and rewritten to the cache with a new expiration time. /// the image is downloaded again and rewritten to the cache with a new expiration time.
/// </summary> /// </summary>
public static TimeSpan CacheExpiration { get; set; } public static TimeSpan CacheExpiration { get; set; }
/// <summary> /// <summary>
/// The time interval after which a cached image is updated and rewritten to the cache. /// 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 be shorter than the value /// The default value is one day. This time interval should not be greater than the value
/// of the CacheExpiration property. /// of the CacheExpiration property.
/// </summary> /// </summary>
public static TimeSpan CacheUpdateAge { get; set; } public static TimeSpan CacheUpdateAge { get; set; }
static TileImageLoader() static TileImageLoader()
{ {
Cache = MemoryCache.Default; CacheExpiration = TimeSpan.FromDays(7);
CacheExpiration = TimeSpan.FromDays(30d); CacheUpdateAge = TimeSpan.FromDays(1);
CacheUpdateAge = TimeSpan.FromDays(1d);
} }
internal TileImageLoader(TileLayer tileLayer) private readonly ConcurrentQueue<Tile> pendingTiles = new ConcurrentQueue<Tile>();
{ private int threadCount;
this.tileLayer = tileLayer;
}
internal void StartGetTiles(IEnumerable<Tile> tiles) internal void BeginGetTiles(TileLayer tileLayer, IEnumerable<Tile> tiles)
{ {
if (tileLayer.TileSource != null && tiles.Any()) if (tiles.Any())
{ {
ThreadPool.QueueUserWorkItem(GetTilesAsync, tiles.ToList()); // get current TileLayer property values in UI thread
var dispatcher = tileLayer.Dispatcher;
var tileSource = tileLayer.TileSource;
var sourceName = tileLayer.SourceName;
var maxDownloads = tileLayer.MaxParallelDownloads;
var animateOpacity = tileLayer.AnimateTileOpacity;
ThreadPool.QueueUserWorkItem(o =>
GetTiles(tiles.ToList(), dispatcher, tileSource, sourceName, maxDownloads, animateOpacity));
} }
} }
@ -86,79 +85,87 @@ namespace MapControl
while (pendingTiles.TryDequeue(out tile)) ; // no Clear method while (pendingTiles.TryDequeue(out tile)) ; // no Clear method
} }
private string GetCacheKey(Tile tile) private void GetTiles(List<Tile> tiles, Dispatcher dispatcher, TileSource tileSource,
string sourceName, int maxDownloads, bool animateOpacity)
{ {
return string.Format("{0}/{1}/{2}/{3}", tileLayer.SourceName, tile.ZoomLevel, tile.XIndex, tile.Y); var imageTileSource = tileSource as ImageTileSource;
}
private void GetTilesAsync(object tileList) if (imageTileSource != null)
{
var tiles = (List<Tile>)tileList;
var imageTileSource = tileLayer.TileSource as ImageTileSource;
if (imageTileSource != null && !imageTileSource.CanLoadAsync)
{ {
foreach (var tile in tiles) if (!imageTileSource.CanLoadAsync) // call LoadImage in UI thread
{ {
tileLayer.Dispatcher.BeginInvoke( foreach (var tile in tiles)
(Action<Tile, ImageTileSource>)((t, ts) => t.SetImageSource(ts.LoadImage(t.XIndex, t.Y, t.ZoomLevel), tileLayer.AnimateTileOpacity)), {
DispatcherPriority.Background, tile, imageTileSource); dispatcher.BeginInvoke(
(Action<Tile, ImageTileSource>)((t, ts) => t.SetImageSource(LoadImage(ts, t), animateOpacity)),
DispatcherPriority.Background, tile, imageTileSource);
}
return;
} }
} }
else else if (!tileSource.UriFormat.StartsWith("file:")) // always load local image files asynchronously
{ {
if (imageTileSource == null && Cache != null && if (Cache == null || string.IsNullOrWhiteSpace(sourceName))
!string.IsNullOrWhiteSpace(tileLayer.SourceName) &&
!tileLayer.TileSource.UriFormat.StartsWith("file://"))
{ {
var outdatedTiles = new List<Tile>(tiles.Count); // no caching here: use default asynchronous downloading and caching done by WPF
foreach (var tile in tiles) foreach (var tile in tiles)
{ {
var key = GetCacheKey(tile); dispatcher.BeginInvoke(
var buffer = Cache.Get(key) as byte[]; (Action<Tile, TileSource>)((t, ts) => t.SetImageSource(CreateImage(ts, t), animateOpacity)),
var image = CreateImage(buffer); DispatcherPriority.Background, tile, tileSource);
if (image != null)
{
tileLayer.Dispatcher.BeginInvoke(
(Action<Tile, ImageSource>)((t, i) => t.SetImageSource(i, tileLayer.AnimateTileOpacity)),
DispatcherPriority.Background, tile, image);
long creationTime = BitConverter.ToInt64(buffer, 0);
if (DateTime.FromBinary(creationTime) + CacheUpdateAge < DateTime.UtcNow)
{
// update outdated cache
outdatedTiles.Add(tile);
}
}
else
{
pendingTiles.Enqueue(tile);
}
} }
tiles = outdatedTiles; // enqueue outdated tiles at last return;
} }
var outdatedTiles = new List<Tile>(tiles.Count);
foreach (var tile in tiles) foreach (var tile in tiles)
{ {
pendingTiles.Enqueue(tile); var key = GetCacheKey(sourceName, tile);
var buffer = Cache.Get(key) as byte[];
var image = CreateImage(buffer);
if (image != null)
{
dispatcher.BeginInvoke(
(Action<Tile, ImageSource>)((t, i) => t.SetImageSource(i, animateOpacity)),
DispatcherPriority.Background, tile, image);
long creationTime = BitConverter.ToInt64(buffer, 0);
if (DateTime.FromBinary(creationTime) + CacheUpdateAge < DateTime.UtcNow)
{
outdatedTiles.Add(tile); // update outdated cache
}
}
else
{
pendingTiles.Enqueue(tile); // not yet cached
}
} }
while (downloadThreadCount < Math.Min(pendingTiles.Count, tileLayer.MaxParallelDownloads)) tiles = outdatedTiles; // enqueue outdated tiles at last
{ }
Interlocked.Increment(ref downloadThreadCount);
ThreadPool.QueueUserWorkItem(LoadTiles); foreach (var tile in tiles)
} {
pendingTiles.Enqueue(tile);
}
while (threadCount < Math.Min(pendingTiles.Count, maxDownloads))
{
Interlocked.Increment(ref threadCount);
ThreadPool.QueueUserWorkItem(o => LoadPendingTiles(dispatcher, tileSource, sourceName, animateOpacity));
} }
} }
private void LoadTiles(object o) private void LoadPendingTiles(Dispatcher dispatcher, TileSource tileSource, string sourceName, bool animateOpacity)
{ {
var imageTileSource = tileLayer.TileSource as ImageTileSource; var imageTileSource = tileSource as ImageTileSource;
Tile tile; Tile tile;
while (pendingTiles.TryDequeue(out tile)) while (pendingTiles.TryDequeue(out tile))
@ -168,65 +175,96 @@ namespace MapControl
if (imageTileSource != null) if (imageTileSource != null)
{ {
try image = LoadImage(imageTileSource, tile);
{
image = imageTileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel);
}
catch (Exception ex)
{
Trace.TraceWarning("Loading tile image failed: {0}", ex.Message);
}
} }
else else
{ {
var uri = tileLayer.TileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel); var uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
if (uri != null) if (uri != null)
{ {
if (uri.Scheme == "http") if (uri.Scheme == "file")
{
image = CreateImage(uri.AbsolutePath);
}
else
{ {
buffer = DownloadImage(uri); buffer = DownloadImage(uri);
image = CreateImage(buffer); image = CreateImage(buffer);
} }
else
{
image = CreateImage(uri);
}
} }
} }
if (image != null) if (image != null || !tile.HasImageSource) // do not set null if tile already has an image (from cache)
{ {
tileLayer.Dispatcher.BeginInvoke( dispatcher.BeginInvoke(
(Action<Tile, ImageSource>)((t, i) => t.SetImageSource(i, tileLayer.AnimateTileOpacity)), (Action<Tile, ImageSource>)((t, i) => t.SetImageSource(i, animateOpacity)),
DispatcherPriority.Background, tile, image); DispatcherPriority.Background, tile, image);
}
if (buffer != null && Cache != null) if (buffer != null && image != null)
{ {
Cache.Set(GetCacheKey(tile), buffer, new CacheItemPolicy { SlidingExpiration = CacheExpiration }); Cache.Set(GetCacheKey(sourceName, tile), buffer, new CacheItemPolicy { SlidingExpiration = CacheExpiration });
}
} }
} }
Interlocked.Decrement(ref downloadThreadCount); Interlocked.Decrement(ref threadCount);
} }
private static ImageSource CreateImage(Uri uri) private static string GetCacheKey(string sourceName, Tile tile)
{ {
var image = new BitmapImage(); return string.Format("{0}/{1}/{2}/{3}", sourceName, tile.ZoomLevel, tile.XIndex, tile.Y);
}
private static ImageSource LoadImage(ImageTileSource tileSource, Tile tile)
{
ImageSource image = null;
try try
{ {
image.BeginInit(); image = tileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel);
image.CacheOption = BitmapCacheOption.OnLoad; }
image.UriSource = uri; catch (Exception ex)
image.EndInit(); {
image.Freeze(); Trace.TraceWarning("Loading tile image failed: {0}", ex.Message);
}
return image;
}
private static ImageSource CreateImage(TileSource tileSource, Tile tile)
{
ImageSource image = null;
try
{
image = BitmapFrame.Create(tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel));
} }
catch (Exception ex) catch (Exception ex)
{ {
Trace.TraceWarning("Creating tile image failed: {0}", ex.Message); Trace.TraceWarning("Creating tile image failed: {0}", ex.Message);
image = null; }
return image;
}
private static ImageSource CreateImage(string path)
{
ImageSource image = null;
if (File.Exists(path))
{
try
{
using (var stream = new FileStream(path, FileMode.Open))
{
image = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
}
catch (Exception ex)
{
Trace.TraceWarning("Creating tile image failed: {0}", ex.Message);
}
} }
return image; return image;
@ -234,7 +272,7 @@ namespace MapControl
private static ImageSource CreateImage(byte[] buffer) private static ImageSource CreateImage(byte[] buffer)
{ {
BitmapImage image = null; ImageSource image = null;
if (buffer != null && buffer.Length > sizeof(long)) if (buffer != null && buffer.Length > sizeof(long))
{ {
@ -242,18 +280,12 @@ namespace MapControl
{ {
using (var stream = new MemoryStream(buffer, sizeof(long), buffer.Length - sizeof(long), false)) using (var stream = new MemoryStream(buffer, sizeof(long), buffer.Length - sizeof(long), false))
{ {
image = new BitmapImage(); image = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Trace.TraceWarning("Creating tile image failed: {0}", ex.Message); Trace.TraceWarning("Creating tile image failed: {0}", ex.Message);
image = null;
} }
} }
@ -269,11 +301,6 @@ namespace MapControl
var request = (HttpWebRequest)WebRequest.Create(uri); var request = (HttpWebRequest)WebRequest.Create(uri);
request.UserAgent = "XAML Map Control"; request.UserAgent = "XAML Map Control";
if (Cache != null)
{
request.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore);
}
using (var response = (HttpWebResponse)request.GetResponse()) using (var response = (HttpWebResponse)request.GetResponse())
using (var responseStream = response.GetResponseStream()) using (var responseStream = response.GetResponseStream())
{ {
@ -289,8 +316,6 @@ namespace MapControl
buffer = length > 0 ? memoryStream.GetBuffer() : memoryStream.ToArray(); buffer = length > 0 ? memoryStream.GetBuffer() : memoryStream.ToArray();
} }
} }
//Trace.TraceInformation("Downloaded {0}", uri);
} }
catch (WebException ex) catch (WebException ex)
{ {

View file

@ -40,7 +40,7 @@ namespace MapControl
} }
private readonly MatrixTransform transform = new MatrixTransform(); private readonly MatrixTransform transform = new MatrixTransform();
private readonly TileImageLoader tileImageLoader; private readonly TileImageLoader tileImageLoader = new TileImageLoader();
private List<Tile> tiles = new List<Tile>(); private List<Tile> tiles = new List<Tile>();
private string description = string.Empty; private string description = string.Empty;
private Int32Rect grid; private Int32Rect grid;
@ -48,7 +48,6 @@ namespace MapControl
public TileLayer() public TileLayer()
{ {
tileImageLoader = new TileImageLoader(this);
MinZoomLevel = 1; MinZoomLevel = 1;
MaxZoomLevel = 18; MaxZoomLevel = 18;
MaxParallelDownloads = 8; MaxParallelDownloads = 8;
@ -96,7 +95,7 @@ namespace MapControl
{ {
SelectTiles(); SelectTiles();
RenderTiles(); RenderTiles();
tileImageLoader.StartGetTiles(tiles.Where(t => !t.HasImage)); tileImageLoader.BeginGetTiles(this, tiles.Where(t => !t.HasImageSource));
} }
} }

View file

@ -102,7 +102,7 @@ namespace MapControl
private Uri GetOpenStreetMapUri(int x, int y, int zoomLevel) private Uri GetOpenStreetMapUri(int x, int y, int zoomLevel)
{ {
var hostIndex = (x + y + zoomLevel) % 3; var hostIndex = (x + y) % 3;
return new Uri(UriFormat. return new Uri(UriFormat.
Replace("{c}", "abc".Substring(hostIndex, 1)). Replace("{c}", "abc".Substring(hostIndex, 1)).
@ -113,7 +113,7 @@ namespace MapControl
private Uri GetGoogleMapsUri(int x, int y, int zoomLevel) private Uri GetGoogleMapsUri(int x, int y, int zoomLevel)
{ {
var hostIndex = (x + y + zoomLevel) % 4; var hostIndex = (x + y) % 4;
return new Uri(UriFormat. return new Uri(UriFormat.
Replace("{i}", hostIndex.ToString()). Replace("{i}", hostIndex.ToString()).
@ -124,7 +124,7 @@ namespace MapControl
private Uri GetMapQuestUri(int x, int y, int zoomLevel) private Uri GetMapQuestUri(int x, int y, int zoomLevel)
{ {
var hostIndex = (x + y + zoomLevel) % 4 + 1; var hostIndex = (x + y) % 4 + 1;
return new Uri(UriFormat. return new Uri(UriFormat.
Replace("{n}", hostIndex.ToString()). Replace("{n}", hostIndex.ToString()).

View file

@ -39,8 +39,8 @@
<Compile Include="..\AnimationEx.WinRT.cs"> <Compile Include="..\AnimationEx.WinRT.cs">
<Link>AnimationEx.WinRT.cs</Link> <Link>AnimationEx.WinRT.cs</Link>
</Compile> </Compile>
<Compile Include="..\Freezable.cs"> <Compile Include="..\ImageTileSource.cs">
<Link>Freezable.cs</Link> <Link>ImageTileSource.cs</Link>
</Compile> </Compile>
<Compile Include="..\Int32Rect.cs"> <Compile Include="..\Int32Rect.cs">
<Link>Int32Rect.cs</Link> <Link>Int32Rect.cs</Link>

View file

@ -8,8 +8,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")] [assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.7.0")] [assembly: AssemblyVersion("1.8.0")]
[assembly: AssemblyFileVersion("1.7.0")] [assembly: AssemblyFileVersion("1.8.0")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]

View file

@ -8,8 +8,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")] [assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.7.0")] [assembly: AssemblyVersion("1.8.0")]
[assembly: AssemblyFileVersion("1.7.0")] [assembly: AssemblyFileVersion("1.8.0")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]

View file

@ -8,8 +8,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")] [assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.7.0")] [assembly: AssemblyVersion("1.8.0")]
[assembly: AssemblyFileVersion("1.7.0")] [assembly: AssemblyFileVersion("1.8.0")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]

View file

@ -8,8 +8,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")] [assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.7.0")] [assembly: AssemblyVersion("1.8.0")]
[assembly: AssemblyFileVersion("1.7.0")] [assembly: AssemblyFileVersion("1.8.0")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]

View file

@ -8,8 +8,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")] [assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.7.0")] [assembly: AssemblyVersion("1.8.0")]
[assembly: AssemblyFileVersion("1.7.0")] [assembly: AssemblyFileVersion("1.8.0")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Runtime.Caching;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
@ -22,6 +23,9 @@ namespace WpfApplication
{ {
switch (Properties.Settings.Default.TileCache) switch (Properties.Settings.Default.TileCache)
{ {
case "MemoryCache":
TileImageLoader.Cache = MemoryCache.Default;
break;
case "FileDbCache": case "FileDbCache":
TileImageLoader.Cache = new FileDbCache(TileImageLoader.DefaultCacheName, TileImageLoader.DefaultCacheDirectory); TileImageLoader.Cache = new FileDbCache(TileImageLoader.DefaultCacheName, TileImageLoader.DefaultCacheDirectory);
break; break;

View file

@ -8,8 +8,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCompany("Clemens Fischer")]
[assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")] [assembly: AssemblyCopyright("Copyright © Clemens Fischer 2012-2013")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.7.0")] [assembly: AssemblyVersion("1.8.0")]
[assembly: AssemblyFileVersion("1.7.0")] [assembly: AssemblyFileVersion("1.8.0")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]