// 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; } } }