using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Windows.Forms; using Core.Common.Utils; using Core.Common.Utils.Collections; using Core.GIS.GeoAPI.Extensions.Feature; using Core.GIS.GeoAPI.Geometries; using Core.GIS.NetTopologySuite.Geometries; using Core.GIS.SharpMap.Api.Layers; using Core.GIS.SharpMap.CoordinateSystems.Transformations; using Core.GIS.SharpMap.Layers; using Core.GIS.SharpMap.Rendering; using Core.GIS.SharpMap.Styles; using Core.GIS.SharpMap.UI.Forms; using log4net; using GeometryFactory = Core.GIS.SharpMap.Converters.Geometries.GeometryFactory; namespace Core.GIS.SharpMap.UI.Tools { /// /// Abstract baseclass for IMaptool implementations to interact with map /// public abstract class MapTool : IMapTool { private static readonly ILog log = LogManager.GetLogger(typeof(MapTool)); private bool isActive; public Map.Map Map { get { return MapControl != null ? MapControl.Map : null; } } public virtual IMapControl MapControl { get; set; } public string Name { get; set; } /// /// Returns true if tool is currently busy (working). /// public virtual bool IsBusy { get; protected set; } /// /// Returns true if tool is currently enabled or greyed out. /// public virtual bool Enabled { get { return true; } } /// /// Use this property to enable or disable tool. /// public virtual bool IsActive { get { if (AlwaysActive) { return true; } return isActive; } set { if (AlwaysActive) { throw new InvalidOperationException("Use Execute() method instead of Activating AlwaysActive tools!"); } isActive = value; } } public virtual bool AlwaysActive { get { return false; } } public virtual bool RendersInScreenCoordinates { get { return false; } } /// /// Map tool may be applied only to a set of layers. This property allows to define a filter for these layers. /// Then the layers can be obtained using property. /// public Func LayerFilter { get; set; } /// /// Returns visible layers which satisfy . /// public virtual IEnumerable Layers { get { var allLayers = Map.GetAllVisibleLayers(true); return LayerFilter == null ? allLayers : allLayers.Where(LayerFilter); } } public virtual Cursor Cursor { get; set; } public IFeature FindNearestFeature(ICoordinate worldPos, float limit, out ILayer outLayer, Func condition) { IFeature nearestFeature = null; outLayer = null; // Since we are only interested in one geometry object start with the topmost trackersLayer and stop // searching the lower layers when an object is found. foreach (var mapLayer in Map.GetAllVisibleLayers(true).OrderBy(l => l.RenderOrder)) { if (mapLayer is VectorLayer) { var vectorLayer = mapLayer as VectorLayer; IEnvelope envelope; float localLimit = limit; if ((!vectorLayer.IsSelectable) || ((null != condition) && (!condition(vectorLayer)))) { continue; } // Adjust the marge limit for Layers with a symbol style and if the size of the symbol exceeds // the minimum limit. Ignore layers with custom renderers if ((vectorLayer.Style.Symbol != null) && (0 == vectorLayer.CustomRenderers.Count)) { var size = MapHelper.ImageToWorld(MapControl.Map, vectorLayer.Style.Symbol.Width, vectorLayer.Style.Symbol.Height); if ((size.X > localLimit) || (size.Y > localLimit)) { envelope = MapHelper.GetEnvelope(worldPos, size.X, size.Y); localLimit = (float) Math.Max(envelope.Width, envelope.Height); } else { envelope = MapHelper.GetEnvelope(worldPos, localLimit); } } else { envelope = MapHelper.GetEnvelope(worldPos, localLimit); } if (vectorLayer.DataSource != null) { var layerEnvelope = vectorLayer.Envelope; if (layerEnvelope != null && !layerEnvelope.Intersects(envelope)) { continue; } // Get features in the envelope var objectsAt = vectorLayer.GetFeatures(envelope).ToList(); // Mousedown at new position if (null != objectsAt) { IFeature feature = null; if (objectsAt.Count == 1) { feature = objectsAt.First(); } else if (objectsAt.Count > 1) { double localDistance; feature = FindNearestFeature(vectorLayer, objectsAt.Distinct(), worldPos, localLimit, out localDistance); } if (null != feature) { nearestFeature = feature; outLayer = vectorLayer; break; } } } } } return nearestFeature; } /// /// Returns the next feature at worldPos. /// /// /// /// /// the layer containing the next feature; null if no next feature is found. /// /// /// the next feature at worldPos, null if there is no next feature. public IFeature GetNextFeatureAtPosition(ICoordinate worldPos, float limit, out ILayer outLayer, IFeature feature, Func condition) { var envelope = MapHelper.GetEnvelope(worldPos, limit); IFeature nextFeature = null; var featureFound = false; outLayer = null; foreach (var mapLayer in Map.GetAllVisibleLayers(false)) { var vectorLayer = mapLayer as VectorLayer; if (vectorLayer == null || !vectorLayer.IsSelectable || vectorLayer.DataSource == null || (condition != null && !condition(vectorLayer))) { continue; } var point = GeometryFactory.CreatePoint(worldPos); var objectsAt = vectorLayer.GetFeatures(envelope); foreach (var featureAt in objectsAt) { // GetFeatures(envelope) uses the geometry bounds; this results in more // geometries than we actually are interested in (especially linestrings and polygons). var geometry = featureAt.Geometry; if (mapLayer.CoordinateTransformation != null) { geometry = GeometryTransform.TransformGeometry(geometry, mapLayer.CoordinateTransformation.MathTransform); } if (geometry.Distance(point) >= limit) { continue; } if (featureFound) { nextFeature = featureAt; outLayer = vectorLayer; return nextFeature; } if (featureAt == feature) { featureFound = true; continue; } if (nextFeature != null) { continue; } // If feature is last in collections objectsAt nextfeature is first nextFeature = featureAt; outLayer = vectorLayer; } } return nextFeature; } public virtual void OnMouseDown(ICoordinate worldPosition, MouseEventArgs e) {} public virtual void OnBeforeMouseMove(ICoordinate worldPosition, MouseEventArgs e, ref bool handled) {} public virtual void OnMouseUp(ICoordinate worldPosition, MouseEventArgs e) {} public virtual void OnMouseWheel(ICoordinate worldPosition, MouseEventArgs e) {} public virtual void OnMouseMove(ICoordinate worldPosition, MouseEventArgs e) {} public virtual void OnMouseDoubleClick(object sender, MouseEventArgs e) {} public virtual void OnMouseHover(ICoordinate worldPosition, EventArgs e) {} public virtual void OnDragEnter(DragEventArgs e) {} public virtual void OnDragDrop(DragEventArgs e) {} public virtual void OnKeyDown(KeyEventArgs e) {} public virtual void OnKeyUp(KeyEventArgs e) {} public virtual void OnPaint(PaintEventArgs e) {} public virtual void OnMapLayerRendered(Graphics g, ILayer layer) {} public virtual void OnMapPropertyChanged(object sender, PropertyChangedEventArgs e) {} public virtual void OnMapCollectionChanged(object sender, NotifyCollectionChangingEventArgs e) {} public virtual IEnumerable GetContextMenuItems(ICoordinate worldPosition) { yield break; } public virtual void Execute() {} public virtual void ActiveToolChanged(IMapTool newTool) {} protected bool IsCtrlPressed { get { return (Control.ModifierKeys & Keys.Control) == Keys.Control; } } protected bool IsAltPressed { get { return (Control.ModifierKeys & Keys.Alt) == Keys.Alt; } } protected bool IsShiftPressed { get { return (Control.ModifierKeys & Keys.Shift) == Keys.Shift; } } /// /// Converts a coordinate as clicked on the map (so in the maps coordinate system) to coordinates /// in the layer's coordinate system. /// protected static ICoordinate ConvertFromMapToLayer(MapTool mapTool, ICoordinate mapCoordinate) { var layer = mapTool.Layers.FirstOrDefault(); if (layer == null) { return mapCoordinate; } if (layer.CoordinateTransformation != null) { var xy = layer.CoordinateTransformation.MathTransform.Inverse() .Transform(new[] { mapCoordinate.X, mapCoordinate.Y }); return new Coordinate(xy[0], xy[1]); } return mapCoordinate; } protected bool AdditionalButtonsBeingPressed(MouseEventArgs e) { return (e.Button != MouseButtons.None && e.Button != MouseButtons.Left) || (IsCtrlPressed && IsAltPressed); } protected ToolStripMenuItem CreateContextMenuItemForFeaturesAtLocation(ICoordinate worldPosition, string menuName, Action onFeatureClick, bool includeSelectedFeatures, Func filterFeature = null) { var limit = (float) MapHelper.ImageToWorld(Map, 10); var menuItemsToAdd = new List(); menuItemsToAdd.AddRange(GetSelectableFeaturesMenuItems(worldPosition, limit, null, null, onFeatureClick, filterFeature)); if (menuItemsToAdd.Count > 0) { menuItemsToAdd.Insert(0, new ToolStripSeparatorWithText { Text = "At location", Height = 30, ForeColor = Color.Black }); } if (!includeSelectedFeatures) { return CreateDropDownMenuItem(menuName, menuItemsToAdd); } var selectedFeatures = (MapControl.SelectedFeatures != null ? MapControl.SelectedFeatures.Where(feature => filterFeature == null || !filterFeature(Map.GetLayerByFeature(feature, true), feature)) : Enumerable.Empty()).ToList(); if (selectedFeatures.Any()) { // add selected items menuItemsToAdd.Add(new ToolStripSeparatorWithText { Text = "From selection", Height = 30, ForeColor = Color.Black }); menuItemsToAdd.AddRange(selectedFeatures.Select(feature => CreateFeatureToolStripMenuItem(feature, Map.GetLayerByFeature(feature, true), onFeatureClick))); } return CreateDropDownMenuItem(menuName, menuItemsToAdd); } /// /// Find the nearest feature to worldPos out of a collection of candidates. If there is no geometry /// with a distance less than limit null is returned. /// private static IFeature FindNearestFeature(VectorLayer vectorLayer, IEnumerable candidates, ICoordinate worldPos, float limit, out double distance) { var point = GeometryFactory.CreatePoint(worldPos.X, worldPos.Y); IFeature current = null; distance = double.MaxValue; foreach (IFeature feature in candidates) { var geometry = vectorLayer.CustomRenderers.Count > 0 ? vectorLayer.CustomRenderers[0].GetRenderedFeatureGeometry(feature, vectorLayer) : (vectorLayer.CoordinateTransformation != null ? GeometryTransform.TransformGeometry(feature.Geometry, vectorLayer.CoordinateTransformation.MathTransform) : feature.Geometry); var localDistance = geometry.Distance(point); if ((localDistance < distance) && (localDistance < limit)) { current = feature; distance = localDistance; } } return current; } private static ToolStripMenuItem CreateDropDownMenuItem(string menuName, IEnumerable dropDownItems) { var toolStripItems = dropDownItems.ToArray(); var menu = new ToolStripMenuItem(menuName); menu.DropDownItems.AddRange(toolStripItems); return toolStripItems.Length == 0 ? null : menu; } private IEnumerable GetSelectableFeaturesMenuItems(ICoordinate worldPosition, float limit, IFeature currentFeature, IFeature startFeature, Action onFeatureClick, Func filterFeature) { ILayer selectedLayer; var nextFeature = GetNextFeatureAtPosition(worldPosition, limit, out selectedLayer, currentFeature, ol => ol.Visible && ol.Selectable); if (nextFeature == null || Equals(nextFeature, startFeature)) { yield break; } if (filterFeature == null || !filterFeature(selectedLayer, nextFeature)) { yield return CreateFeatureToolStripMenuItem(nextFeature, selectedLayer, onFeatureClick); } if (startFeature == null) { startFeature = nextFeature; } var nextFeatures = GetSelectableFeaturesMenuItems(worldPosition, limit, nextFeature, startFeature, onFeatureClick, filterFeature); const int maxNumberOfItems = 25; var count = 0; foreach (var toolStripMenuItem in nextFeatures) { yield return toolStripMenuItem; count++; if (count == maxNumberOfItems) { var message = string.Format("Showing only the first {0} features found.", maxNumberOfItems); yield return new ToolStripMenuItem { Text = message, Height = 30, ForeColor = Color.LightGray, ToolTipText = "Zoom in to access currently hidden features." }; yield break; } } } private static ToolStripMenuItem CreateFeatureToolStripMenuItem(IFeature feature, ILayer layer, Action onClick) { var featureName = GetFeatureName(feature, layer); var symbol = GetSymbol(feature, layer); return new ToolStripMenuItem(featureName, symbol, (sender, args) => onClick(layer, feature)); } private static Image GetSymbol(IFeature feature, ILayer layer) { if (!(layer is VectorLayer)) { return null; } var vectorLayer = ((VectorLayer) layer); try { var currentVectorStyle = vectorLayer.Theme != null ? vectorLayer.Theme.GetStyle(feature) as VectorStyle : vectorLayer.Style; return currentVectorStyle != null ? currentVectorStyle.LegendSymbol : null; } catch (Exception) { return null; } } private static string GetFeatureName(IFeature feature, ILayer layer) { var featureName = feature.Name; var layerName = layer == null ? "-" : layer.Name; return string.Format("{0} ({1})", featureName, layerName); } #region toolDrawing /// /// Stores a clone of the map backgroud during dragging draw operations /// private Bitmap backGroundImage; /// /// Stores a (empty) bitmap during dragging draw operations; only purpose is to prevent creation of large bitmap /// for every drawing operation /// private Bitmap drawingBitmap; private bool dragging; public virtual void StartDrawing() { if (Map.Image.PixelFormat == PixelFormat.Undefined) { log.Error("drawCache is broken ..."); } backGroundImage = (Bitmap) Map.Image.Clone(); drawingBitmap = new Bitmap(Map.Image.Width, Map.Image.Height); dragging = true; } public virtual void Render(Graphics graphics, Map.Map mapBox) {} public virtual void OnDraw(Graphics graphics) // TODO: remove it, use OnPaint? {} public virtual void DoDrawing(bool drawTools) { if (!dragging) { return; } Graphics graphics = Graphics.FromImage(drawingBitmap); // use transform from map; this enables directly calling ILayer.OnRender graphics.Transform = Map.MapTransform.Clone(); graphics.Clear(Color.Transparent); graphics.PageUnit = GraphicsUnit.Pixel; graphics.Clear(MapControl.BackColor); graphics.DrawImage(backGroundImage, 0, 0); if (drawTools) { foreach (IMapTool tool in MapControl.Tools) { if (tool.IsActive) { tool.Render(graphics, Map); } } } OnDraw(graphics); Graphics graphicsMap = MapControl.CreateGraphics(); graphicsMap.DrawImage(drawingBitmap, 0, 0); graphicsMap.Dispose(); graphics.Dispose(); } public virtual void StopDrawing() { if (backGroundImage != null) { backGroundImage.Dispose(); backGroundImage = null; } if (drawingBitmap != null) { drawingBitmap.Dispose(); drawingBitmap = null; } dragging = false; } public virtual void Cancel() {} #endregion } }