Making ActiveTool easier to use

Topics: SharpMap Project, WinForms Controls
Aug 29, 2009 at 10:25 AM
Edited Aug 29, 2009 at 10:28 AM

Hi,

I've started using SharpMap for a part-time project which is helping an NGO to implement and monitor the Afforestation/Reforestation programme in South India. It has been an enabling tool for them. So, kudos to all who have been working on this wonderful toolkit.

As I said, since this project is for an NGO, the users of the application are also barely computer literate. Therefore user friendliness is very important. Currently, the WinForms MapImage control supports switching the ActiveTool using a property. This warrants some kind of a button in a toolbar where the user selects what tool they want to use. I want to make it a bit more seamless because my requirements are simpler.

I don't need the ZoomIn and ZoomOut tools, because users can use the mouse wheel or a slider in the toolbar to do so. So, the only tools we require are Pan & Query. I have a button in the toolbar which toggles this mode, but users find it a bit confusing. So, here is what I want to achieve:

  1. When the user simply clicks without dragging, the Query tool behaviour should be emulated
  2. If the user holds the mouse down and starts dragging then, the Pan tool behaviour should be emulated

I thought this would be more user friendly and natural way of interacting with the map, for my application users atleast.

P.S.: I am using v1.1 (or v1.2 I am not sure) from the trunk folder downloaded using TortoiseSVN.

Apr 22, 2010 at 3:32 PM

I fully agree with you regarding this 

Developer
Apr 22, 2010 at 7:56 PM

Me too. I've built such a mapcontrol in another project. For the basic functions - pan, zoom-in, zoom-out and query - you need no buttons at all. Here is another OSS project that pans and zooms like this. There are + and - buttons too, but you do not need them, and they are stateless. There is no query yet, but the way it can be done is

  1. On mouse-up and checking if the drag distance is zero (or within some small radius). 
  2. On mouse-down and check if you clicked a queriable object. You have to restrict the queriable objects to certain layers, and probably to point geometries. 

I think I prefer the first. You could also combine mouse-up and queriable objects to prevent unwanted queries. 

What about the mouse wheel behavior. MapImage's mousewheeling is inverted to google and bings. Is this not confusing for your users?

btw, great to hear you are putting SharpMap to good use :)

Paul

Apr 23, 2010 at 2:34 PM

The map control you are talking about is it open source?  

Anyway the behaviuor i need is

1) Pan, zoom-in/out and query buttonless...     

2) Query events including multi layer query...     on layer with the query flag enabled ( is saw the new property IsQueryEnabled  in the i can query interface! this should be nice to be used! )

So the events could be changed from

   public delegate void MapQueryHandler(FeatureDataTable data);

to something like:

 public delegate void MapQueryHandler(List<QueryResult>);

Where the Query result includes    the  FeatureDataTable data   and the Layer Name....

 

3) On mouse-down and check if you clicked a queriable object. 

4) On mouse-up checking if the drag distance is zero (or within some small radius). 

For the Wheel the default should be Bing / Google way...   but a simply flat like "ReverseWheel" it is costless...

 

I CAN WORK on the Map Image in order to make this modifications....      in the end..   i NEED IT... so...

Apr 24, 2010 at 12:36 PM
pauldendulk wrote:

Me too. I've built such a mapcontrol in another project. For the basic functions - pan, zoom-in, zoom-out and query - you need no buttons at all. Here is another OSS project that pans and zooms like this. There are + and - buttons too, but you do not need them, and they are stateless. There is no query yet, but the way it can be done is

  1. On mouse-up and checking if the drag distance is zero (or within some small radius). 
  2. On mouse-down and check if you clicked a queriable object. You have to restrict the queriable objects to certain layers, and probably to point geometries. 

I think I prefer the first. You could also combine mouse-up and queriable objects to prevent unwanted queries. 

What about the mouse wheel behavior. MapImage's mousewheeling is inverted to google and bings. Is this not confusing for your users?

btw, great to hear you are putting SharpMap to good use :)

Paul

Hi Paul,

I ended up doing something similar. I made a copy of MapImage.cs in my project and applied similar logic like the one you described above. The user's have now find the application intuitive enough!

The SharpMap team has definitely done a great job with the toolkit. Thanks for all of you.

Cheers,

Raghu

Apr 26, 2010 at 7:48 AM

kraghavk...   and your work is open source?....

Developer
Apr 26, 2010 at 10:17 PM

hi people!

About MapQueryHandler, we could also return the original DataSet and use the FeatureDataTable.TableName as LayerName.

The BruTile client is open source but very different in architecture from the SharpMap client. It might be used to get ideas. What I find important in a map control is asynchronous retrieval and rendering. While panning you drag a buffer around while the next image is retrieved in the background.

drop by on irc if you are interested in contributing.

Paul

May 1, 2010 at 8:16 AM
Edited May 1, 2010 at 8:17 AM
sandex wrote:

kraghavk...   and your work is open source?....

Well, I usually work on this project during the weekends. I have a full time day job. So, I would say that though I have implemented the required behaviour for my target user's I always hold back from posting code from this project because I always feel it might have something or the other missing to be put out as open source. Anyways, here is the code of the MapView control. It is the MapImage control of the v0.9 trunk. I have just modified the MouseDown, MouseUp and MouseMove events to suite my requirements:

 

 

using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.VisualBasic.Devices;
using SharpMap;
using SharpMap.Data;
using SharpMap.Data.Providers;
using SharpMap.Forms;
using SharpMap.Layers;

namespace MyMappingApp
{
[DesignTimeVisible(true)]
public class MapViewer : PictureBox
{
// Fields private SharpMap.Forms.MapImage.Tools _Activetool;
private double _fineZoomFactor = 10.0;
private Map _Map;
private int _queryLayerIndex;
private double _wheelZoomMagnitude = 2.0;
private System.Drawing.Point mousedrag;
private bool mousedragging = false;
private Image mousedragImg;

// Events public event MapImage.ActiveToolChangedHandler ActiveToolChanged;
public event MapImage.MapCenterChangedHandler MapCenterChanged;
public event MapImage.MapQueryHandler MapQueried;
public event EventHandler MapRefreshed;
public event MapImage.MapZoomHandler MapZoomChanged;
public new event MapImage.MouseEventHandler MouseDown;
public new event MapImage.MouseEventHandler MouseMove;
public new event MapImage.MouseEventHandler MouseUp;

// Methods public MapViewer()
{
this._Map = new Map(base.Size);
this._Activetool = SharpMap.Forms.MapImage.Tools.None;
base.MouseMove += new MouseEventHandler(this.MapImage_MouseMove);
base.MouseUp += new MouseEventHandler(this.MapImage_MouseUp);
base.MouseDown += new MouseEventHandler(this.MapImage_MouseDown);
base.MouseWheel += new MouseEventHandler(this.MapImage_Wheel);
this.DoubleBuffered = true;
}

internal GeometryFeatureProvider GetGeometryProvider(string layerName)
{
VectorLayer layer = this.Map.Layers[layerName] as VectorLayer;

if (layer != null)
return layer.DataSource as GeometryFeatureProvider;
else return null;
}

private void MapImage_MouseDown(object sender, MouseEventArgs e)
{
if (this._Map != null)
{
if (e.Button == MouseButtons.Left)
this.mousedrag = e.Location;

SharpMap.Geometries.Point worldPos = this._Map.ImageToWorld((PointF)new Point(e.X, e.Y));
OnMouseDown(worldPos, e);
}
}

private void MapImage_MouseMove(object sender, MouseEventArgs e)
{
if (this._Map != null)
{
SharpMap.Geometries.Point worldPos = this._Map.ImageToWorld(new PointF((float)e.X, (float)e.Y));

OnMouseMove(worldPos, e);

if ((((base.Image != null) && (e.Location != this.mousedrag)) && !this.mousedragging) && (e.Button == MouseButtons.Left))
{
this.mousedragImg = base.Image.Clone() as Image;
this.mousedragging = true;
}
if (this.mousedragging)
{
this.ActiveTool = SharpMap.Forms.MapImage.Tools.Pan;

Image image = new Bitmap(base.Size.Width, base.Size.Height);
Graphics graphics = Graphics.FromImage(image);
graphics.Clear(Color.Transparent);
graphics.DrawImageUnscaled(this.mousedragImg, new Point(e.Location.X - this.mousedrag.X, e.Location.Y - this.mousedrag.Y));
graphics.Dispose();
base.Image = image;
}
else { this.ActiveTool = SharpMap.Forms.MapImage.Tools.Query;
}
}
}

private void MapImage_MouseUp(object sender, MouseEventArgs e)
{
if (this._Map != null)
{
SharpMap.Geometries.Point worldPos = this._Map.ImageToWorld((PointF)new Point(e.X, e.Y));
OnMouseUp(worldPos, e);

if (e.Button == MouseButtons.Left)
{
if (this.ActiveTool == SharpMap.Forms.MapImage.Tools.Pan)
{
if (this.mousedragging)
{
Point point = new Point((base.Width / 2) + (this.mousedrag.X - e.Location.X), (base.Height / 2) + (this.mousedrag.Y - e.Location.Y));
this._Map.Center = this._Map.ImageToWorld((PointF)point);

OnMapCenterChanged();
}

this.ActiveTool = SharpMap.Forms.MapImage.Tools.Query;
this.Refresh();
}
else if (this.ActiveTool == SharpMap.Forms.MapImage.Tools.Query)
{
if ((this._Map.Layers.Count > this._queryLayerIndex) && (this._queryLayerIndex > -1))
{
if (this._Map.Layers[this._queryLayerIndex].GetType() == typeof(VectorLayer))
{
VectorLayer layer = this._Map.Layers[this._queryLayerIndex] as VectorLayer;
SharpMap.Geometries.BoundingBox box = this._Map.ImageToWorld((PointF)new Point(e.X, e.Y)).GetBoundingBox().Grow(this._Map.PixelSize * 5.0);
FeatureDataSet ds = new FeatureDataSet();
layer.DataSource.Open();
layer.DataSource.ExecuteIntersectionQuery(box, ds);
layer.DataSource.Close();

FeatureDataTable tbl = (ds.Tables.Count > 0) ? ds.Tables[0] : new FeatureDataTable();

OnMapQueried(tbl);
}
}
else { //MessageBox.Show("No active layer to query"); } } } if (this.mousedragImg != null)
{
this.mousedragImg.Dispose();
this.mousedragImg = null;
}
this.mousedragging = false;
}
}

private Keyboard keyboard = new Keyboard();
private void MapImage_Wheel(object sender, MouseEventArgs e)
{
if (this._Map != null)
{
double y = ((double)e.Delta) / 120.0;
double x = 1.0 + (_wheelZoomMagnitude / (10.0 * (keyboard.CtrlKeyDown ? _fineZoomFactor : 1.0)));
this.Zoom *= Math.Pow(x, y);
}
}

protected override void OnMouseHover(EventArgs e)
{
if (!this.Focused)
{
base.Focus();
}
base.OnMouseHover(e);
}

public override void Refresh()
{
if (this._Map != null)
{
this._Map.Size = base.Size;

if ((this._Map.Layers == null) || (this._Map.Layers.Count == 0))
{
base.Image = null;
}
else { base.Image = this._Map.GetMap();
}

base.Refresh();

OnMapRefreshed();
}
}

// Properties public SharpMap.Forms.MapImage.Tools ActiveTool
{
get { return this._Activetool;
}
set { bool flag = value != this._Activetool;
this._Activetool = value;
if (value == SharpMap.Forms.MapImage.Tools.Pan)
{
this.Cursor = Cursors.SizeAll;
}
else { this.Cursor = Cursors.Default;
}

if (flag) OnActiveToolChanged();
}
}

[DefaultValue(10), Category("Behavior"), Description("The amount which the WheelZoomMagnitude is divided by when the Control key is pressed. A number greater than 1 decreases the zoom, and less than 1 increases it. A negative number reverses it.")]
public double FineZoomFactor
{
get { return this._fineZoomFactor;
}
set { this._fineZoomFactor = value;
}
}

[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Map Map
{
get { return this._Map;
}
set { this._Map = value;
if (this._Map != null)
{
this.Refresh();
}
}
}

private SharpMap.Geometries.Point centroidBeforeZoom;

public double Zoom
{
get { return Map.Zoom; }
set
{
centroidBeforeZoom = _Map.Center;
Map.Zoom = value;
OnMapZoomChanged();
}
}

public int QueryLayerIndex
{
get { return this._queryLayerIndex;
}
set { this._queryLayerIndex = value;
}
}

[Category("Behavior"), Description("The amount which a single movement of the mouse wheel zooms by."), DefaultValue(2)]
public double WheelZoomMagnitude
{
get { return this._wheelZoomMagnitude;
}
set { this._wheelZoomMagnitude = value;
}
}

internal void ZoomToExtents()
{
           this.Map.ZoomToExtents();
         this.Map.Zoom *= 1.1;
}

internal void ZoomToBox(SharpMap.Geometries.BoundingBox bbox)
{
this.Map.ZoomToBox(bbox);
this.Map.Zoom *= 1.1;
   }

#region "On" methods for events
protected virtual void OnActiveToolChanged()
{
if (ActiveToolChanged != null)
ActiveToolChanged(ActiveTool);
}

protected virtual void OnMapCenterChanged()
{
if (MapCenterChanged != null)
MapCenterChanged(this._Map.Center);
}

protected virtual void OnMapQueried(FeatureDataTable data)
{
if (MapQueried != null)
MapQueried(data);
}

protected virtual void OnMapRefreshed()
{
if (MapRefreshed != null)
MapRefreshed(this, EventArgs.Empty);
}

protected virtual void OnMapZoomChanged()
{
this._Map.Center = centroidBeforeZoom;
OnMapCenterChanged();

Refresh();

if (MapZoomChanged != null)
MapZoomChanged(this.Zoom);
}

protected virtual void OnMouseDown(SharpMap.Geometries.Point worldPos, MouseEventArgs imagePos)
{
if (MouseDown != null)
MouseDown(worldPos, imagePos);
}

protected virtual void OnMouseMove(SharpMap.Geometries.Point worldPos, MouseEventArgs imagePos)
{
if (MouseMove != null)
MouseMove(worldPos, imagePos);
}

protected virtual void OnMouseUp(SharpMap.Geometries.Point worldPos, MouseEventArgs imagePos)
{
if (MouseUp != null)
MouseUp(worldPos, imagePos);
}
#endregion
} }

 

Cheers,
Raghu