// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk)
//
// This file is part of SharpMap.
// SharpMap is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// SharpMap is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
// You should have received a copy of the GNU Lesser General Public License
// along with SharpMap; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using GeoAPI.Geometries;
using NetTopologySuite;
using SharpMap.Layers;
using SharpMap.Rendering;
using SharpMap.Rendering.Decoration;
using SharpMap.Styles;
using SharpMap.Utilities;
using Point = GeoAPI.Geometries.Coordinate;
using System.Drawing.Imaging;
using Common.Logging;
using System.Reflection;
namespace SharpMap
{
///
/// Map class, the main holder for a MapObject in SharpMap
///
///
/// Creating a new map instance, adding layers and rendering the map:
///
[Serializable]
public class Map : IDisposable
{
///
/// Method to invoke the static constructor of this class
///
public static void Configure()
{
// Methods sole purpose is to get the static constructor executed
}
///
/// Static constructor. Needed to get set.
///
static Map()
{
try
{
_logger.Debug("Trying to get GeoAPI.GeometryServiceProvider.Instance");
var instance = GeoAPI.GeometryServiceProvider.Instance;
if (instance == null)
{
_logger.Debug("Returned null");
throw new InvalidOperationException();
}
}
catch (InvalidOperationException)
{
_logger.Debug("Loading NetTopologySuite");
Assembly.Load("NetTopologySuite");
_logger.Debug("Loaded NetTopologySuite");
_logger.Debug("Trying to get GeoAPI.GeometryServiceProvider.Instance");
var instance = GeoAPI.GeometryServiceProvider.Instance;
if (instance == null)
{
_logger.Debug("Returned null");
throw new InvalidOperationException();
}
}
// The following code did not seem to work in all cases.
/*
if (System.ComponentModel.LicenseManager.UsageMode != System.ComponentModel.LicenseUsageMode.Designtime)
{
_logger.Debug("In design mode");
Trace.WriteLine("In design mode");
// We have to do this initialization with reflection due to the fact that NTS can reference an older version of GeoAPI and redirection
// is not available at design time..
var ntsAssembly = Assembly.Load("NetTopologySuite");
_logger.Debug("Loaded NetTopologySuite");
Trace.WriteLine("Loaded NetTopologySuite");
try
{
_logger.Debug("Trying to access GeoAPI.GeometryServiceProvider.Instance");
Trace.WriteLine("Trying to access GeoAPI.GeometryServiceProvider.Instance");
if (GeoAPI.GeometryServiceProvider.Instance == null)
{
_logger.Debug("Returned null, setting it to default");
Trace.WriteLine("Returned null, setting it to default");
var ntsApiGeometryServices = ntsAssembly.GetType("NetTopologySuite.NtsGeometryServices");
GeoAPI.GeometryServiceProvider.Instance =
ntsApiGeometryServices.GetProperty("Instance").GetValue(null, null) as
GeoAPI.IGeometryServices;
}
}
catch (InvalidOperationException)
{
_logger.Debug("InvalidOperationException thrown, setting it to default");
Trace.WriteLine("InvalidOperationException thrown, setting it to default");
var ntsApiGeometryServices = ntsAssembly.GetType("NetTopologySuite.NtsGeometryServices");
GeoAPI.GeometryServiceProvider.Instance =
ntsApiGeometryServices.GetProperty("Instance").GetValue(null, null) as
GeoAPI.IGeometryServices;
}
_logger.Debug("Exiting design mode handling");
Trace.WriteLine("Exiting design mode handling");
}
*/
}
static readonly ILog _logger = LogManager.GetLogger(typeof(Map));
///
/// Used for converting numbers to/from strings
///
public static NumberFormatInfo NumberFormatEnUs = new CultureInfo("en-US", false).NumberFormat;
#region Fields
private readonly List _decorations = new List();
private Color _backgroundColor;
private int _srid = -1;
private double _zoom;
private Point _center;
private readonly LayerCollection _layers;
private readonly LayerCollection _backgroundLayers;
private readonly VariableLayerCollection _variableLayers;
#pragma warning disable 169
// both fields redundant, but unable to remove without violating serialization
private Matrix _mapTransform;
private Matrix _mapTransformInverted;
#pragma warning restore 169
private readonly object _lockMapTransform = new object();
private readonly object _lockMapTransformInverted = new object();
private float[] _mapTransformElements;
private float[] _mapTransformInvertedElements;
private readonly MapViewPortGuard _mapViewportGuard;
private readonly Dictionary> _layersPerGroup = new Dictionary>();
private ObservableCollection _replacingCollection;
private Guid _id = Guid.NewGuid();
#endregion
///
/// Specifies whether to trigger a dispose on all layers (and their datasources) contained in this map when the map-object is disposed.
/// The default behaviour is true unless the map is a result of a Map.Clone() operation in which case the value is false
///
/// If you reuse your datasources or layers between many map-objects you should set this property to false in order for them to keep existing after a map.dispose()
///
public bool DisposeLayersOnDispose = true;
///
/// Initializes a new map
///
public Map() : this(new Size(640, 480))
{
}
///
/// Initializes a new map
///
/// Size of map in pixels
public Map(Size size)
{
_mapViewportGuard = new MapViewPortGuard(size, 0d, Double.MaxValue);
if (LicenseManager.UsageMode != LicenseUsageMode.Designtime)
{
Factory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(_srid);
}
_layers = new LayerCollection();
_layersPerGroup.Add(_layers, new List());
_backgroundLayers = new LayerCollection();
_layersPerGroup.Add(_backgroundLayers, new List());
_variableLayers = new VariableLayerCollection(_layers);
_layersPerGroup.Add(_variableLayers, new List());
BackColor = Color.Transparent;
var matrix = new Matrix();
_mapTransformElements = matrix.Elements;
_mapTransformInvertedElements = matrix.Elements;
_center = new Point(0, 0);
_zoom = 1;
WireEvents();
if (_logger.IsDebugEnabled)
_logger.DebugFormat("Map initialized with size {0},{1}", size.Width, size.Height);
}
///
/// Wires the events
///
private void WireEvents()
{
_backgroundLayers.CollectionChanged += OnLayersCollectionChanged;
_layers.CollectionChanged += OnLayersCollectionChanged;
}
///
/// Event handler to intercept when a new ITileAsymclayer is added to the Layers List and associate the MapNewTile Handler Event
///
private void OnLayersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
var cloneList = _layersPerGroup[sender];
IterUnHookEvents(sender, cloneList);
}
if (e.Action == NotifyCollectionChangedAction.Replace || e.Action == NotifyCollectionChangedAction.Remove)
{
IterUnHookEvents(sender, e.OldItems.Cast());
}
if (e.Action == NotifyCollectionChangedAction.Replace || e.Action == NotifyCollectionChangedAction.Add)
{
IterWireEvents(sender, e.NewItems.Cast());
}
}
private void IterWireEvents(object owner, IEnumerable layers)
{
foreach (var layer in layers)
{
_layersPerGroup[owner].Add(layer);
var tileAsyncLayer = layer as ITileAsyncLayer;
if (tileAsyncLayer != null)
{
WireTileAsyncEvents(tileAsyncLayer);
}
var group = layer as LayerGroup;
if (group != null)
{
group.LayersChanging += OnLayerGroupCollectionReplaching;
group.LayersChanged += OnLayerGroupCollectionReplached;
var nestedList = group.Layers;
if (group.Layers != null)
{
group.Layers.CollectionChanged += OnLayersCollectionChanged;
_layersPerGroup.Add(nestedList, new List());
}
else
{
_layersPerGroup.Add(nestedList, new List());
}
IterWireEvents(nestedList, nestedList);
}
}
}
private void IterUnHookEvents(object owner, IEnumerable layers)
{
var toBeRemoved = new List();
foreach (var layer in layers)
{
toBeRemoved.Add(layer);
var tileAsyncLayer = layer as ITileAsyncLayer;
if (tileAsyncLayer != null)
{
UnhookTileAsyncEvents(tileAsyncLayer);
}
var group = layer as LayerGroup;
if (group != null)
{
group.LayersChanging -= OnLayerGroupCollectionReplaching;
group.LayersChanged -= OnLayerGroupCollectionReplached;
var nestedList = group.Layers;
if (nestedList != null)
{
nestedList.CollectionChanged -= OnLayersCollectionChanged;
IterUnHookEvents(nestedList, nestedList);
_layersPerGroup.Remove(nestedList);
}
}
}
var clonedList = _layersPerGroup[owner];
toBeRemoved.ForEach(layer => clonedList.Remove(layer));
}
private void OnLayerGroupCollectionReplached(object sender, EventArgs eventArgs)
{
var layerGroup = (LayerGroup)sender;
var newCollection = layerGroup.Layers;
IterUnHookEvents(_replacingCollection, _replacingCollection);
_layersPerGroup.Remove(_replacingCollection);
_replacingCollection.CollectionChanged -= OnLayersCollectionChanged;
if (newCollection != null)
{
IterWireEvents(newCollection, newCollection);
_layersPerGroup.Add(newCollection, new List(newCollection));
newCollection.CollectionChanged += OnLayersCollectionChanged;
}
}
private void OnLayerGroupCollectionReplaching(object sender, EventArgs eventArgs)
{
var layerGroup = (LayerGroup)sender;
_replacingCollection = layerGroup.Layers;
}
private void layer_DownloadProgressChanged(int tilesRemaining)
{
if (tilesRemaining <= 0)
{
OnRefreshNeeded(EventArgs.Empty);
}
}
private void WireTileAsyncEvents(ITileAsyncLayer tileAsyncLayer)
{
if (tileAsyncLayer.OnlyRedrawWhenComplete)
{
tileAsyncLayer.DownloadProgressChanged += layer_DownloadProgressChanged;
}
else
{
tileAsyncLayer.MapNewTileAvaliable += MapNewTileAvaliableHandler;
}
}
private void UnhookTileAsyncEvents(ITileAsyncLayer tileAsyncLayer)
{
tileAsyncLayer.DownloadProgressChanged -= layer_DownloadProgressChanged;
tileAsyncLayer.MapNewTileAvaliable -= MapNewTileAvaliableHandler;
}
#region IDisposable Members
///
/// Disposes the map object
///
public void Dispose()
{
if (DisposeLayersOnDispose)
{
if (Layers != null)
{
foreach (IDisposable disposable in Layers.OfType())
{
disposable.Dispose();
}
}
if (BackgroundLayer != null)
{
foreach (IDisposable disposable in BackgroundLayer.OfType())
{
disposable.Dispose();
}
}
if (VariableLayers != null)
{
foreach (IDisposable layer in VariableLayers.OfType())
layer.Dispose();
}
}
if (Layers != null)
{
Layers.Clear();
}
if (BackgroundLayer != null)
{
BackgroundLayer.Clear();
}
if (VariableLayers != null)
{
VariableLayers.Clear();
}
}
#endregion
#region Events
#region Delegates
///
/// EventHandler for event fired when the maps layer list has been changed
///
public delegate void LayersChangedEventHandler();
///
/// EventHandler for event fired when all layers have been rendered
///
public delegate void MapRenderedEventHandler(Graphics g);
///
/// EventHandler for event fired when all layers are about to be rendered
///
public delegate void MapRenderingEventHandler(Graphics g);
///
/// EventHandler for event fired when the zoomlevel or the center point has been changed
///
public delegate void MapViewChangedHandler();
#endregion
///
/// Event fired when the maps layer list have been changed
///
[Obsolete("This event is never invoked since it has been made impossible to change the LayerCollection for a map instance.")]
#pragma warning disable 67
public event LayersChangedEventHandler LayersChanged;
#pragma warning restore 67
///
/// Event fired when the zoomlevel or the center point has been changed
///
public event MapViewChangedHandler MapViewOnChange;
///
/// Event fired when all layers are about to be rendered
///
public event MapRenderedEventHandler MapRendering;
///
/// Event fired when all layers have been rendered
///
public event MapRenderedEventHandler MapRendered;
///
/// Event fired when one layer have been rendered
///
public event EventHandler LayerRendering;
///
/// Event fired when one layer have been rendered
///
public event EventHandler LayerRenderedEx;
///
/// Event fired when a layer has been rendered
///
[Obsolete("Use LayerRenderedEx")]
public event EventHandler LayerRendered;
///
/// Event fired when a new Tile is available in a TileAsyncLayer
///
public event MapNewTileAvaliabledHandler MapNewTileAvaliable;
///
/// Event that is called when a layer has changed and the map need to redraw
///
public event EventHandler RefreshNeeded;
#endregion
#region Methods
///
/// Renders the map to an image
///
/// the map image
public Image GetMap()
{
Image img = new Bitmap(Size.Width, Size.Height);
Graphics g = Graphics.FromImage(img);
RenderMap(g);
g.Dispose();
return img;
}
///
/// Renders the map to an image with the supplied resolution
///
/// The resolution of the image
/// The map image
public Image GetMap(int resolution)
{
Image img = new Bitmap(Size.Width, Size.Height);
((Bitmap)img).SetResolution(resolution, resolution);
Graphics g = Graphics.FromImage(img);
RenderMap(g);
g.Dispose();
return img;
}
///
/// Renders the map to a Metafile (Vectorimage).
///
///
/// A Metafile can be saved as WMF,EMF etc. or put onto the clipboard for paste in other applications such av Word-processors which will give
/// a high quality vector image in that application.
///
/// The current map rendered as to a Metafile
public Metafile GetMapAsMetafile()
{
return GetMapAsMetafile(String.Empty);
}
///
/// Renders the map to a Metafile (Vectorimage).
///
/// The filename of the metafile. If this is null or empty the metafile is not saved.
///
/// A Metafile can be saved as WMF,EMF etc. or put onto the clipboard for paste in other applications such av Word-processors which will give
/// a high quality vector image in that application.
///
/// The current map rendered as to a Metafile
public Metafile GetMapAsMetafile(string metafileName)
{
Metafile metafile;
var bm = new Bitmap(1, 1);
using (var g = Graphics.FromImage(bm))
{
var hdc = g.GetHdc();
using (var stream = new MemoryStream())
{
metafile = new Metafile(stream, hdc, new RectangleF(0, 0, Size.Width, Size.Height),
MetafileFrameUnit.Pixel, EmfType.EmfPlusDual);
using (var metafileGraphics = Graphics.FromImage(metafile))
{
metafileGraphics.PageUnit = GraphicsUnit.Pixel;
metafileGraphics.TransformPoints(CoordinateSpace.Page, CoordinateSpace.Device,
new[] { new PointF(Size.Width, Size.Height) });
//Render map to metafile
RenderMap(metafileGraphics);
}
//Save metafile if desired
if (!String.IsNullOrEmpty(metafileName))
File.WriteAllBytes(metafileName, stream.ToArray());
}
g.ReleaseHdc(hdc);
}
return metafile;
}
//ToDo: fill in the blanks
///
///
///
///
///
///
///
///
public void MapNewTileAvaliableHandler(ITileAsyncLayer sender, Envelope bbox, Bitmap bm, int sourceWidth, int sourceHeight, ImageAttributes imageAttributes)
{
var e = MapNewTileAvaliable;
if (e != null)
e(sender, bbox, bm, sourceWidth, sourceHeight, imageAttributes);
}
///
/// Renders the map using the provided object.
///
/// the object to use
/// if object is null.
/// if there are no layers to render.
public void RenderMap(Graphics g)
{
OnMapRendering(g);
if (g == null)
throw new ArgumentNullException("g", "Cannot render map with null graphics object!");
//Pauses the timer for VariableLayer
_variableLayers.Pause = true;
if ((Layers == null || Layers.Count == 0) && (BackgroundLayer == null || BackgroundLayer.Count == 0) && (_variableLayers == null || _variableLayers.Count == 0))
throw new InvalidOperationException("No layers to render");
g.Transform = MapTransform;
var mvp = (MapViewport) this;
g.Clear(BackColor);
g.PageUnit = GraphicsUnit.Pixel;
double zoom = mvp.Zoom;
double scale = double.NaN; //will be resolved if needed
ILayer[] layerList;
if (_backgroundLayers != null && _backgroundLayers.Count > 0)
{
layerList = new ILayer[_backgroundLayers.Count];
_backgroundLayers.CopyTo(layerList, 0);
foreach (ILayer layer in layerList)
{
if (layer.VisibilityUnits == VisibilityUnits.Scale && double.IsNaN(scale))
{
scale = mvp.GetMapScale((int)g.DpiX);
}
double visibleLevel = layer.VisibilityUnits == VisibilityUnits.ZoomLevel ? zoom : scale;
OnLayerRendering(layer, LayerCollectionType.Background);
if (layer.Enabled)
{
if (layer.MaxVisible >= visibleLevel && layer.MinVisible < visibleLevel)
{
LayerCollectionRenderer.RenderLayer(layer, g, mvp);
}
}
OnLayerRendered(layer, LayerCollectionType.Background);
}
}
if (_layers != null && _layers.Count > 0)
{
layerList = new ILayer[_layers.Count];
_layers.CopyTo(layerList, 0);
//int srid = (Layers.Count > 0 ? Layers[0].SRID : -1); //Get the SRID of the first layer
foreach (ILayer layer in layerList)
{
if (layer.VisibilityUnits == VisibilityUnits.Scale && double.IsNaN(scale))
{
scale = mvp.GetMapScale((int)g.DpiX);
}
double visibleLevel = layer.VisibilityUnits == VisibilityUnits.ZoomLevel ? zoom : scale;
OnLayerRendering(layer, LayerCollectionType.Static);
if (layer.Enabled && layer.MaxVisible >= visibleLevel && layer.MinVisible < visibleLevel)
LayerCollectionRenderer.RenderLayer(layer, g, mvp);
OnLayerRendered(layer, LayerCollectionType.Static);
}
}
if (_variableLayers != null && _variableLayers.Count > 0)
{
layerList = new ILayer[_variableLayers.Count];
_variableLayers.CopyTo(layerList, 0);
foreach (ILayer layer in layerList)
{
if (layer.VisibilityUnits == VisibilityUnits.Scale && double.IsNaN(scale))
{
scale = mvp.GetMapScale((int)g.DpiX);
}
double visibleLevel = layer.VisibilityUnits == VisibilityUnits.ZoomLevel ? zoom : scale;
if (layer.Enabled && layer.MaxVisible >= visibleLevel && layer.MinVisible < visibleLevel)
LayerCollectionRenderer.RenderLayer(layer, g, mvp);
OnLayerRendered(layer, LayerCollectionType.Variable);
}
}
#pragma warning disable 612,618
RenderDisclaimer(g);
#pragma warning restore 612,618
// Render all map decorations
foreach (var mapDecoration in _decorations)
{
mapDecoration.Render(g, mvp);
}
//Resets the timer for VariableLayer
_variableLayers.Pause = false;
OnMapRendered(g);
}
///
/// Fires the RefreshNeeded event.
///
/// EventArgs argument.
protected virtual void OnRefreshNeeded(EventArgs e)
{
var handler = RefreshNeeded;
if (handler != null)
handler(this, e);
}
///
/// Fired when map is rendering
///
///
protected virtual void OnMapRendering(Graphics g)
{
var e = MapRendering;
if (e != null) e(g);
}
///
/// Fired when Map is rendered
///
///
protected virtual void OnMapRendered(Graphics g)
{
var e = MapRendered;
if (e != null) e(g); //Fire render event
}
///
/// Method called when starting to render of . This fires the
/// event.
///
/// The layer to render
/// The collection type
protected virtual void OnLayerRendering(ILayer layer, LayerCollectionType layerCollectionType)
{
var e = LayerRendering;
if (e != null) e(this, new LayerRenderingEventArgs(layer, layerCollectionType));
}
#pragma warning disable 612,618
///
/// Method called when of has been rendered. This fires the
/// and event.
///
/// The layer to render
/// The collection type
protected virtual void OnLayerRendered(ILayer layer, LayerCollectionType layerCollectionType)
{
var e = LayerRendered;
#pragma warning restore 612,618
if (e != null)
{
e(this, EventArgs.Empty);
}
var eex = LayerRenderedEx;
if (eex != null)
{
eex(this, new LayerRenderingEventArgs(layer, layerCollectionType));
}
}
///
/// Renders the map using the provided object.
///
/// the object to use
/// the to use
/// if object is null.
/// if there are no layers to render.
public void RenderMap(Graphics g, LayerCollectionType layerCollectionType)
{
RenderMap(g, layerCollectionType, true, false);
}
///
/// Renders the map using the provided object.
///
/// the object to use
/// the to use
/// Set whether to draw map decorations on the map (if such are set)
/// Set whether to draw with transparent background or with BackColor as background
/// if object is null.
/// if there are no layers to render.
public void RenderMap(Graphics g, LayerCollectionType layerCollectionType, bool drawMapDecorations, bool drawTransparent)
{
if (g == null)
throw new ArgumentNullException("g", "Cannot render map with null graphics object!");
_variableLayers.Pause = true;
LayerCollection lc = null;
switch (layerCollectionType)
{
case LayerCollectionType.Static:
lc = Layers;
break;
case LayerCollectionType.Variable:
lc = VariableLayers;
break;
case LayerCollectionType.Background:
lc = BackgroundLayer;
break;
}
if (lc == null || lc.Count == 0)
throw new InvalidOperationException("No layers to render");
var origTransform = g.Transform;
g.Transform = MapTransform;
var mvp = (MapViewport) this;
if (!drawTransparent)
g.Clear(BackColor);
g.PageUnit = GraphicsUnit.Pixel;
//LayerCollectionRenderer.AllowParallel = layerCollectionType == LayerCollectionType.Static;
using (var lcr = new LayerCollectionRenderer(lc))
{
lcr.Render(g, mvp, layerCollectionType == LayerCollectionType.Static);
}
/*
var layerList = new ILayer[lc.Count];
lc.CopyTo(layerList, 0);
foreach (ILayer layer in layerList)
{
if (layer.Enabled && layer.MaxVisible >= Zoom && layer.MinVisible < Zoom)
layer.Render(g, this);
}
*/
if (drawTransparent)
g.Transform = origTransform;
if (layerCollectionType == LayerCollectionType.Static)
{
#pragma warning disable 612,618
RenderDisclaimer(g);
#pragma warning restore 612,618
if (drawMapDecorations)
{
foreach (var mapDecoration in Decorations)
{
mapDecoration.Render(g, mvp);
}
}
}
_variableLayers.Pause = false;
}
///
/// Returns a cloned copy of this map-object.
/// Layers are not cloned. The same instances are referenced from the cloned copy as from the original.
/// The property is however false on this object (which prevents layers beeing disposed and then not usable from the original map)
///
/// Instance of
public Map Clone()
{
Map clone;
lock (_lockMapTransform)
{
clone = new Map
{
BackColor = BackColor,
#pragma warning disable 612,618
Disclaimer = Disclaimer,
DisclaimerLocation = DisclaimerLocation,
#pragma warning restore 612,618
MaximumZoom = MaximumZoom,
MinimumZoom = MinimumZoom,
MaximumExtents = MaximumExtents,
EnforceMaximumExtents = EnforceMaximumExtents,
PixelAspectRatio = PixelAspectRatio,
Zoom = Zoom,
DisposeLayersOnDispose = false,
SRID = SRID,
_id = ID
};
#pragma warning disable 612,618
if (DisclaimerFont != null)
clone.DisclaimerFont = (Font)DisclaimerFont.Clone();
#pragma warning restore 612,618
if (MapTransform != null)
clone.MapTransform = MapTransform;
if (!Size.IsEmpty)
clone.Size = new Size(Size.Width, Size.Height);
if (Center != null)
clone.Center = Center.Copy();
}
if (BackgroundLayer != null)
clone.BackgroundLayer.AddCollection(BackgroundLayer.Clone());
for (int i = 0; i < Decorations.Count; i++)
clone.Decorations.Add(Decorations[i]);
if (Layers != null)
clone.Layers.AddCollection(Layers.Clone());
if (VariableLayers != null)
clone.VariableLayers.AddCollection(VariableLayers.Clone());
return clone;
}
[Obsolete]
private void RenderDisclaimer(Graphics g)
{
//Disclaimer
if (!String.IsNullOrEmpty(_disclaimer))
{
var size = VectorRenderer.SizeOfString(g, _disclaimer, _disclaimerFont);
size.Width = (Single)Math.Ceiling(size.Width);
size.Height = (Single)Math.Ceiling(size.Height);
StringFormat sf;
switch (DisclaimerLocation)
{
case 0: //Right-Bottom
sf = new StringFormat();
sf.Alignment = StringAlignment.Far;
g.DrawString(Disclaimer, DisclaimerFont, Brushes.Black,
g.VisibleClipBounds.Width,
g.VisibleClipBounds.Height - size.Height - 2, sf);
break;
case 1: //Right-Top
sf = new StringFormat();
sf.Alignment = StringAlignment.Far;
g.DrawString(Disclaimer, DisclaimerFont, Brushes.Black,
g.VisibleClipBounds.Width, 0f, sf);
break;
case 2: //Left-Top
g.DrawString(Disclaimer, DisclaimerFont, Brushes.Black, 0f, 0f);
break;
case 3://Left-Bottom
g.DrawString(Disclaimer, DisclaimerFont, Brushes.Black, 0f,
g.VisibleClipBounds.Height - size.Height - 2);
break;
}
}
}
///
/// Returns an enumerable for all layers containing the search parameter in the LayerName property
///
/// Search parameter
/// IEnumerable
public IEnumerable FindLayer(string layername)
{
return Layers.Where(l => l.LayerName.Contains(layername));
}
///
/// Returns a layer by its name
///
/// Name of layer
/// Layer
public ILayer GetLayerByName(string name)
{
ILayer lay = null;
if (Layers != null)
{
lay = Layers.GetLayerByName(name);
}
if (lay == null && BackgroundLayer != null)
{
lay = BackgroundLayer.GetLayerByName(name);
}
if (lay == null && VariableLayers != null)
{
lay = VariableLayers.GetLayerByName(name);
}
return lay;
}
///
/// Zooms to the extents of all layers
///
public void ZoomToExtents()
{
ZoomToBox(GetExtents(), true);
}
///
/// Zooms the map to fit a bounding box
///
///
/// NOTE: If the aspect ratio of the box and the aspect ratio of the mapsize
/// isn't the same, the resulting map-envelope will be adjusted so that it contains
/// the bounding box, thus making the resulting envelope larger!
///
///
/// True if any map rotation should be taken into account (eg ZoomToExtents).
/// False if rotation has already been accounted for (eg Zoom prev / next stack, or non-rotated views)
public void ZoomToBox(Envelope bbox, bool careAboutTransform = false)
{
if (bbox == null || bbox.IsNull) return;
if (careAboutTransform && !MapTransformRotation.Equals(0f))
{
// adjust box for rotated views to ensure it is fully visible at maximum possible zoom
var rad = NetTopologySuite.Utilities.Degrees.ToRadians(MapTransformRotation);
var newWidth = bbox.Width * Math.Abs(Math.Cos(rad)) +
bbox.Height * Math.Abs(Math.Sin(rad));
var newHeight = bbox.Width * Math.Abs(Math.Sin(rad)) +
bbox.Height * Math.Abs(Math.Cos(rad));
bbox = new Envelope(
bbox.Centre.X - newWidth * 0.5,
bbox.Centre.X + newWidth * 0.5,
bbox.Centre.Y - newHeight * 0.5,
bbox.Centre.Y + newHeight * 0.5);
}
//Ensure aspect ratio
var resX = Size.Width == 0 ? double.MaxValue : bbox.Width / Size.Width;
var resY = Size.Height == 0 ? double.MaxValue : bbox.Height / Size.Height;
var zoom = bbox.Width;
if (resY > resX && resX > 0)
zoom *= resY / resX;
var center = new Coordinate(bbox.Centre);
zoom = _mapViewportGuard.VerifyZoom(zoom, center);
var changed = false;
if (zoom != _zoom)
{
_zoom = zoom;
changed = true;
}
if (!center.Equals2D(_center))
{
_center = center;
changed = true;
}
if (changed && MapViewOnChange != null)
MapViewOnChange();
}
///
/// Converts an array of world coordinates to image coordinates based on the current , ,
/// map , and (optionally) the .
///
/// Coordinate array in world coordinates
/// Indicates whether should be applied. True for typical coordinate calcs,
/// False when rendering to image as the Graphics object has already applied the MapTransform
/// PointF array in image coordinates
public PointF[] WorldToImage(Coordinate[] coordinates, bool careAboutMapTransform = false)
{
if (MapTransformRotation.Equals(0f))
{
// simple case is non-rotated views (ie MapTransform does NOT need to be applied)
var left = Center.X - Zoom * 0.5;
var top = Center.Y + MapHeight * 0.5;
return Transform.WorldToMap(coordinates, left, top, PixelWidth, PixelHeight);
}
// for rotated views, measurements must be performed in the rotated world coordinate space regardless
// of careAboutTransform. The most efficient way is using affine transformation (matrix) to translate,
// ROTATE, scale and flip coordinates, and finally translate to Image centre.
// If careAboutMapTransform == false then the matrix must also apply a reverse rotation as the MapTransform
// will be applied by the graphics object at completion of all drawing
var matrix = Transform.WorldToMapMatrix(
Center, PixelWidth, PixelHeight, MapTransformRotation, Size, careAboutMapTransform);
return Transform.WorldToMap(coordinates, matrix);
}
///
/// Converts a point in world coordinates to image coordinates based on the current , ,
/// map , and (optionally) the .
///
/// Point in world coordinates
/// Indicates whether should be applied. When rendering to image,
/// the Graphics object has usually applied MapTransform
/// PointF in image coordinates
public PointF WorldToImage(Coordinate p, bool careAboutMapTransform = false)
{
var points = WorldToImage(new Coordinate[] {p}, careAboutMapTransform);
return points[0];
}
///
/// Converts a point array from image coordinates to world coordinates based on the current , ,
/// map , and (optionally) the .
///
/// Point array in image coordinates. Note: if you wish to preserve the input values then
/// you must clone the point array as it will be modified if a MapTransform is applied
/// Indicates whether should be applied.
/// Point array in world coordinates
public Point[] ImageToWorld(PointF[] points, bool careAboutMapTransform = false)
{
if (careAboutMapTransform && !MapTransformRotation.Equals(0f))
using (var transformInv = MapTransformInverted)
transformInv.TransformPoints(points);
return Transform.MapToWorld(points, this);
}
///
/// Converts a point from image coordinates to world coordinates based on the current , ,
/// map , and (optionally) the .
///
/// Point in image coordinates. Note: if you wish to preserve the input value then
/// you must clone the point as it will be modified if a MapTransform is applied
/// Indicates whether should be applied.
/// Point in world coordinates
public Point ImageToWorld(PointF p, bool careAboutMapTransform = false)
{
var pts = ImageToWorld(new PointF[] { p }, careAboutMapTransform);
return pts[0];
}
#endregion
#region Properties
///
/// Gets or sets the unique identifier of the map.
///
public Guid ID
{
get { return _id; }
set { _id = value; }
}
///
/// Gets or sets the SRID of the map
///
public int SRID
{
get { return _srid; }
set
{
if (_srid == value)
return;
_srid = value;
Factory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(_srid);
}
}
///
/// Factory used to create geometries
///
public IGeometryFactory Factory { get; private set; }
///
/// List of all map decorations
///
public IList Decorations
{
get { return _decorations; }
}
///
/// Gets the rectilinear extents of the current map based on the current ,
/// , map , and (optionally) the
/// If a is applied, the envelope CONTAINING the rotated view
/// will be returned (used by layers to spatially select data) and the aspect ratio will NOT be the
/// same as map . If aspect ratio is important then refer to
/// and
///
public Envelope Envelope
{
get
{
// height has been adjusted for pixelRatio
var height = MapHeight;
if (double.IsNaN(height) || double.IsInfinity(height))
return new Envelope(0, 0, 0, 0);
var ll = new Coordinate(Center.X - Zoom * .5, Center.Y - height * .5);
var ur = new Coordinate(Center.X + Zoom * .5, Center.Y + height * .5);
// simple, non-rotated view
if (MapTransformRotation.Equals(0f))
return new Envelope(ll, ur);
// otherwise derive envelope containing rotated view (required for layers to select data)
if (Size.Width == 0 || Size.Height == 0)
return new Envelope(0, 0, 0, 0);
var rad = (double) -MapTransformRotation * Math.PI / 180.0;
var newWidth = (ur.X - ll.X) * Math.Abs(Math.Cos(rad)) +
(ur.Y - ll.Y) * Math.Abs(Math.Sin(rad));
var newHeight = (ur.X - ll.X) * Math.Abs(Math.Sin(rad)) +
(ur.Y - ll.Y) * Math.Abs(Math.Cos(rad));
ll = new Coordinate(Center.X - newWidth * .5, Center.Y - newHeight * .5);
ur = new Coordinate(Center.X + newWidth * .5, Center.Y + newHeight * .5);
return new Envelope(ll, ur);
}
}
///
/// Using the you can alter the coordinate system of the map rendering.
/// This makes it possible to rotate the image, for instance to have another direction than north upwards.
/// The matrix elements are stored, and a new matrix is instantiated for every request
///
///
/// Rotate the map output +45 degrees around its center (ie north arrow will point to the top-right corner):
///
/// System.Drawing.Drawing2D.Matrix maptransform = new System.Drawing.Drawing2D.Matrix(); //Create transformation matrix
/// maptransform.RotateAt(45,new PointF(myMap.Size.Width/2,myMap.Size.Height/2)); //Apply 45 degrees rotation around the center of the map
/// myMap.MapTransform = maptransform; //Apply transformation to map
///
///
public Matrix MapTransform
{
get
{
lock (_lockMapTransform)
return new Matrix(
_mapTransformElements[0],
_mapTransformElements[1],
_mapTransformElements[2],
_mapTransformElements[3],
_mapTransformElements[4],
_mapTransformElements[5]);
}
set
{
lock (_lockMapTransform)
{
if (value == null)
value = new Matrix();
if (!value.IsInvertible)
throw new ArgumentException("Matrix not invertible", nameof(value));
_mapTransformElements = value.Elements;
lock (_lockMapTransformInverted)
{
var inverted = value.Clone();
inverted.Invert();
_mapTransformInvertedElements = inverted.Elements;
}
if (value.IsIdentity)
MapTransformRotation = 0f;
else
{
var rad = value.Elements[1] >= 0 ? Math.Acos(value.Elements[0]) : -Math.Acos(value.Elements[0]);
if (rad < 0)
rad += 2 * Math.PI;
MapTransformRotation = (float)(rad * 180.0 / Math.PI);
}
}
}
}
///
/// The inverse of used for calculations from Image to World.
/// The matrix elements are stored, and a new matrix is instantiated for every request
///
internal Matrix MapTransformInverted
{
get
{
lock (_lockMapTransformInverted)
return new Matrix(
_mapTransformInvertedElements[0],
_mapTransformInvertedElements[1],
_mapTransformInvertedElements[2],
_mapTransformInvertedElements[3],
_mapTransformInvertedElements[4],
_mapTransformInvertedElements[5]);
}
}
///
/// MapTransform Rotation in degrees. Facilitates determining if map is rotated without locking MapTransform.
/// Positive rotation is applied anti-clockwise, with the apparent effect of north arrow rotating clockwise.
///
public float MapTransformRotation { get; private set; }
///
/// A collection of layers. The first layer in the list is drawn first, the last one on top.
///
public LayerCollection Layers
{
get { return _layers; }
}
///
/// Collection of background Layers
///
public LayerCollection BackgroundLayer
{
get { return _backgroundLayers; }
}
///
/// A collection of layers. The first layer in the list is drawn first, the last one on top.
///
public VariableLayerCollection VariableLayers
{
get { return _variableLayers; }
}
///
/// Map background color (defaults to transparent)
///
public Color BackColor
{
get { return _backgroundColor; }
set
{
_backgroundColor = value;
if (MapViewOnChange != null)
MapViewOnChange();
}
}
///
/// A focus point on the map.
///
///
/// This point stays at its local position when zooming by wheel.
/// Possible tiles are fetched in order of the distance to this point.
///
public Coordinate CenterOfInterest
{
get => _centerOfInterest?.Copy() ?? Center;
set
{
if (value == _centerOfInterest)
return;
_centerOfInterest = value;
}
}
///
/// Center of map in WCS
///
public Point Center
{
get { return _center.Copy(); }
set
{
if (value == null)
throw new ArgumentNullException("value");
var newZoom = _zoom;
var newCenter = new Coordinate(value);
newZoom = _mapViewportGuard.VerifyZoom(newZoom, newCenter);
var changed = false;
if (newZoom != _zoom)
{
_zoom = newZoom;
changed = true;
}
if (!newCenter.Equals2D(_center))
{
_center = newCenter;
changed = true;
}
if (changed && MapViewOnChange != null)
MapViewOnChange();
}
}
private static int? _dpiX;
///
/// Gets or Sets the Scale of the map (related to current DPI-settings of rendering)
///
public double MapScale
{
get
{
if (!_dpiX.HasValue)
{
using (var g = Graphics.FromHwnd(IntPtr.Zero))
{
_dpiX = (int)g.DpiX;
}
}
return GetMapScale(_dpiX.Value);
}
set
{
if (!_dpiX.HasValue)
{
using (var g = Graphics.FromHwnd(IntPtr.Zero))
{
_dpiX = (int)g.DpiX;
}
}
Zoom = GetMapZoomFromScale(value, _dpiX.Value);
}
}
///
/// Calculate the Zoom value for a given Scale value
///
///
///
///
public double GetMapZoomFromScale(double scale, int dpi)
{
return ScaleCalculations.GetMapZoomFromScaleNonLatLong(scale, 1, dpi, Size.Width);
}
///
/// Returns the mapscale if the map was to be rendered at the current with the specified DPI-settings
///
///
///
public double GetMapScale(int dpi)
{
return ScaleCalculations.CalculateScaleNonLatLong(Zoom, Size.Width, 1, dpi);
}
///
/// Gets or sets the zoom level of map.
///
///
/// The zoom level corresponds to the apparent width of the map in WCS units, regardless of any .
/// Zoom will only equal .Width when is 0 or 180 degrees
/// A zoomlevel of 0 will result in an empty map being rendered, but will not throw an exception
///
public double Zoom
{
get { return _zoom; }
set
{
var newCenter = new Coordinate(_center);
value = _mapViewportGuard.VerifyZoom(value, newCenter);
if (value.Equals(_zoom))
return;
_zoom = value;
if (!newCenter.Equals2D(_center))
_center = newCenter;
if (MapViewOnChange != null)
MapViewOnChange();
}
}
///
/// Get Returns the size of a pixel in world coordinate units
///
public double PixelSize
{
get { return Zoom / Size.Width; }
}
///
/// Returns the width of a pixel in world coordinate units.
///
/// The value returned is the same as .
public double PixelWidth
{
get { return PixelSize; }
}
///
/// Returns the height of a pixel in world coordinate units.
///
/// The value returned is the same as unless is different from 1.
public double PixelHeight
{
get { return PixelSize * _mapViewportGuard.PixelAspectRatio; }
}
///
/// Gets or sets the aspect-ratio of the pixel scales. A value less than
/// 1 will make the map stretch upwards, and larger than 1 will make it smaller.
///
/// Throws an argument exception when value is 0 or less.
public double PixelAspectRatio
{
get { return _mapViewportGuard.PixelAspectRatio; }
set
{
_mapViewportGuard.PixelAspectRatio = value;
}
}
///
/// Height of map in world units
///
///
public double MapHeight
{
get { return (Zoom * Size.Height) / Size.Width * PixelAspectRatio; }
}
///
/// Size of output map
///
public Size Size
{
get { return _mapViewportGuard.Size; }
set { _mapViewportGuard.Size = value; }
}
///
/// Minimum zoom amount allowed
///
public double MinimumZoom
{
get { return _mapViewportGuard.MinimumZoom; }
set
{
_mapViewportGuard.MinimumZoom = value;
}
}
///
/// Maximum zoom amount allowed
///
public double MaximumZoom
{
get { return _mapViewportGuard.MaximumZoom; }
set
{
_mapViewportGuard.MaximumZoom = value;
}
}
///
/// Gets the extents of the map based on the extents of all the layers in the layers collection
///
/// Full map extents
public Envelope GetExtents()
{
if (!_mapViewportGuard.MaximumExtents.IsNull)
return MaximumExtents;
if ((Layers == null || Layers.Count == 0) &&
(VariableLayers == null || VariableLayers.Count == 0) &&
(BackgroundLayer == null || BackgroundLayer.Count == 0))
throw (new InvalidOperationException("No layers to zoom to"));
Envelope bbox = null;
ExtendBoxForCollection(Layers, ref bbox);
ExtendBoxForCollection(VariableLayers, ref bbox);
ExtendBoxForCollection(BackgroundLayer, ref bbox);
return bbox;
}
///
/// Gets or sets a value indicating the maximum visible extent
///
public Envelope MaximumExtents
{
get { return _mapViewportGuard.MaximumExtents; }
set { _mapViewportGuard.MaximumExtents = value; }
}
///
/// Gets or sets a value indicating if should be enforced or not.
///
public bool EnforceMaximumExtents
{
get { return _mapViewportGuard.EnforceMaximumExtents; }
set { _mapViewportGuard.EnforceMaximumExtents = value; }
}
private static void ExtendBoxForCollection(IEnumerable layersCollection, ref Envelope bbox)
{
foreach (var l in layersCollection)
{
//Tries to get bb. Fails on some specific shapes and Mercator projects (World.shp)
Envelope bb;
try
{
bb = l.Envelope;
}
catch (Exception)
{
bb = new Envelope(new Coordinate(-20037508.342789, -20037508.342789), new Coordinate(20037508.342789, 20037508.342789));
}
if (bbox == null)
bbox = bb;
else
{
//FB: bb can be null on empty layers (e.g. temporary working layers with no data objects)
if (bb != null)
bbox.ExpandToInclude(bb);
}
}
}
#endregion
#region Disclaimer
private String _disclaimer;
///
/// Copyright notice to be placed on the map
///
[Obsolete("Use Disclaimer as MapDecoration instead!")]
public String Disclaimer
{
get { return _disclaimer; }
set
{
//only set disclaimer if not already done
if (String.IsNullOrEmpty(_disclaimer))
{
_disclaimer = value;
//Ensure that Font for disclaimer is set
if (_disclaimerFont == null)
_disclaimerFont = new Font(FontFamily.GenericSansSerif, 8f);
}
}
}
private Font _disclaimerFont;
///
/// Font to use for the Disclaimer
///
[Obsolete("Use Disclaimer as MapDecoration instead!")]
public Font DisclaimerFont
{
get { return _disclaimerFont; }
set
{
if (value == null) return;
_disclaimerFont = value;
}
}
private Int32 _disclaimerLocation;
private Coordinate _centerOfInterest;
///
/// Location for the disclaimer
/// 2|1
/// -+-
/// 3|0
///
[Obsolete("Use Disclaimer as MapDecoration instead!")]
public Int32 DisclaimerLocation
{
get { return _disclaimerLocation; }
set { _disclaimerLocation = value % 4; }
}
#endregion
}
///
/// Layer rendering event arguments class
///
public class LayerRenderingEventArgs : EventArgs
{
///
/// The layer that is being or has been rendered
///
public readonly ILayer Layer;
///
/// The layer collection type the layer belongs to.
///
public readonly LayerCollectionType LayerCollectionType;
///
/// Creates an instance of this class
///
/// The layer that is being or has been rendered
/// The layer collection type the layer belongs to.
public LayerRenderingEventArgs(ILayer layer, LayerCollectionType layerCollectionType)
{
Layer = layer;
LayerCollectionType = layerCollectionType;
}
}
}