csmapcontrol

csmapcontrol Git Source Tree

Root/MapControl/MapControl.cs

1//
2// csmapcontrol - a C# map control - http://projects.ciarang.com/p/csmapcontrol
3// Copyright (C) 2010, Ciaran Gultnieks
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18using System;
19using System.Collections.Generic;
20using System.ComponentModel;
21using System.Drawing;
22using System.Windows.Forms;
23using System.Threading;
24using System.Net;
25using System.IO;
26
27namespace csmapcontrol
28{
29
30public class Marker
31{
32public double lat;
33public double lon;
34}
35
36public class TileManager
37{
38
39public delegate void NewDataAvailableHandler();
40public event NewDataAvailableHandler NewDataAvailable;
41
42/// <summary>
43/// Dictionary of cached tiles, keyed on a string formed
44/// as "zoom,x,y".
45/// </summary>
46private Dictionary<string, Bitmap> mTiles;
47
48/// <summary>
49/// Tile currently being downloaded, or null if none.
50/// </summary>
51private string mDownloadTile=null;
52
53public TileManager()
54{
55mTiles=new Dictionary<string, Bitmap>();
56}
57
58public void EmptyCache()
59{
60foreach(string key in mTiles.Keys)
61{
62if(mTiles[key]!=null)
63mTiles[key].Dispose();
64}
65mTiles.Clear();
66}
67
68public Image GetTile(int x,int y,int zoom)
69{
70lock(mTiles)
71{
72string key=zoom.ToString()+','+x.ToString()+','+y.ToString();
73if(mTiles.ContainsKey(key))
74{
75// We already have the tile in our cache...
76return mTiles[key];
77}
78if(mDownloadTile==null)
79{
80// Start downloading the tile...
81mDownloadTile=key;
82Thread t=new Thread(Download);
83t.Start();
84}
85return null;
86}
87}
88
89private void Download()
90{
91try
92{
93string[] vals=mDownloadTile.Split(',');
94string url="http://tile.openstreetmap.org/"+vals[0]+"/"+vals[1]+"/"+vals[2]+".png";
95WebClient client=new WebClient();
96Stream stream=client.OpenRead(url);
97Bitmap tile=new Bitmap(stream);
98stream.Flush();
99stream.Close();
100lock(mTiles)
101{
102mTiles[mDownloadTile]=tile;
103}
104// TODO: If we've reached the (as yet undefined) limit of how
105// many tiles we should be caching, we should remove the least
106// recently used one.
107if(NewDataAvailable!=null)
108NewDataAvailable();
109}
110catch(Exception)
111{
112lock(mTiles)
113{
114mTiles[mDownloadTile]=null;
115}
116if(NewDataAvailable!=null)
117NewDataAvailable();
118}
119finally
120{
121mDownloadTile=null;
122}
123
124}
125
126}
127
128/// <summary>
129/// Map control
130/// </summary>
131public partial class MapControl : UserControl
132{
133
134/// <summary>
135/// A predetermined list of sensible (-ish) discrete scales that can be
136/// used, for example, to provide a linear zoom range for a scrollbar.
137/// </summary>
138public static double[] DiscreteScales={
1390.00005/256,
1400.0005/256,
1410.0008/256,
1420.002/256,
1430.002/256,
1440.003/256,
1450.004/256,
1460.005/256,
1470.006/256,
1480.007/256,
1490.008/256,
1500.009/256,
1510.01/256,
1520.015/256,
1530.02/256,
1540.025/256,
1550.03/256,
1560.035/256,
1570.04/256,
1580.045/256,
1590.05/256,
1600.055/256,
1610.06/256,
1620.065/256,
1630.07/256,
1640.075/256,
1650.08/256,
1660.085/256,
1670.09/256,
1680.095/256,
1690.1/256,
1700.2/256,
1710.3/256,
1720.4/256,
1730.5/256,
1740.5/256,
1750.6/256,
1760.7/256,
1770.8/256,
1780.9/256,
1791.0/256,
1802.0/256,
1813.0/256,
1824.0/256,
1835.0/256,
1846.0/256,
1857.0/256,
1868.0/256,
1879.0/256,
18810.0/256,
18915.0/256,
19020.0/256,
19130.0/256,
19240.0/256,
19350.0/256,
19460.0/256,
19570.0/256,
19680.0/256,
19790.0/256,
198100.0/256,
199110.0/256,
200130.0/256,
201150.0/256,
202170.0/256,
203190.0/256,
204210.0/256,
205240.0/256,
2061};
207
208public List<Marker> Markers
209{
210get { return mMarkers; }
211}
212private List<Marker> mMarkers=new List<Marker>();
213
214/// <summary>
215/// Latitude at centre of view
216/// </summary>
217public double Latitude
218{
219get { return mLat; }
220set { mLat=value; Invalidate(); }
221}
222private double mLat=53.9753;
223
224/// <summary>
225/// Longitude at centre of view
226/// </summary>
227public double Longitude
228{
229get { return mLon; }
230set { mLon=value; Invalidate(); }
231}
232private double mLon=-1.7017;
233
234/// <summary>
235/// Scale - longitude degrees per pixel
236/// </summary>
237public double LonScale
238{
239get { return mScale; }
240set { mScale=value; Invalidate(); }
241}
242private double mScale=1.0/256;
243
244/// <summary>
245/// Tile cache
246/// </summary>
247private static TileManager mTileManager=new TileManager();
248
249private bool mUpdateView=false;
250
251/// <summary>
252/// Event raised when the view parameters are changed INTERNALLY.
253/// For example, by the user dragging the map. This event is not raised
254/// when the change is made by the client, e.g. by setting the relevant
255/// properties.
256/// </summary>
257public event ViewChangedEventHandler ViewChanged;
258public delegate void ViewChangedEventHandler(double lat,double lon,double scale);
259private void RaiseViewChanged()
260{
261if(ViewChanged!=null)
262{
263ViewChanged(mLat,mLon,mScale);
264}
265}
266
267/// <summary>
268/// Event raised when there is diagnostics information available.
269/// </summary>
270public event DiagsInfoEventHandler DiagsInfo;
271public delegate void DiagsInfoEventHandler(string sMsg);
272private void Diags(string msg)
273{
274if(DiagsInfo!=null)
275DiagsInfo(msg);
276}
277
278/// <summary>
279/// Event raised when the user clicks at a location on the map.
280/// </summary>
281public event LocationClickedEventHandler LocationClicked;
282public delegate void LocationClickedEventHandler(double lat,double lon);
283
284/// <summary>
285/// Clear the tile cache. Since the tile manager is static, and shared
286/// between all instances of the control, it will remain in place for
287/// the lifetime of the application unless this is called.
288/// </summary>
289public static void ClearTileCache()
290{
291if(mTileManager!=null)
292mTileManager.EmptyCache();
293}
294
295public MapControl()
296{
297
298InitializeComponent();
299mTileManager.NewDataAvailable+=NewTileDataAvailable;
300}
301
302/// <summary>
303/// Flag that the view needs updating. Calling this will cause a call
304/// to Invalidate() at the next timer tick. We need to go down this
305/// convoluted route rather than simply calling Invalidate() from
306/// the NewTileDataAvailable event handler, because that could cause
307/// it to be called when a paint is still in progress, and this means
308/// the Invalidate gets ignored.
309/// </summary>
310private void UpdateView()
311{
312mUpdateView=true;
313}
314
315private void NewTileDataAvailable()
316{
317if(InvokeRequired)
318{
319BeginInvoke(new MethodInvoker(delegate
320{ UpdateView(); }));
321}
322else
323{
324UpdateView();
325}
326}
327
328/// <summary>
329/// Get the Tile X/Y position for a given latitude and longitude, and
330/// at a given zoom level.
331/// </summary>
332/// <param name="lat">The latitude</param>
333/// <param name="lon">The longitude</param>
334/// <param name="zoom">The zoom level</param>
335/// <param name="x">The tile X</param>
336/// <param name="y">The tile Y</param>
337private void GetTileXYFromLatLon(double lat,double lon,int zoom,out int x,out int y)
338{
339x=(int)((lon+180.0)/360.0*Math.Pow(2.0,zoom));
340y=(int)((1.0-Math.Log(Math.Tan(lat*Math.PI/180.0)+
3411.0/Math.Cos(lat*Math.PI/180.0))/Math.PI)/2.0*Math.Pow(2.0,zoom));
342}
343
344/// <summary>
345/// Get the latitude and longitude for the tile at the given X/Y and
346/// zoom. The lat/lon returned is for the top left corner of the tile.
347/// </summary>
348/// <param name="x">The tile X</param>
349/// <param name="y">The tile Y</param>
350/// <param name="zoom">The zoom level</param>
351/// <param name="lat">The latitude</param>
352/// <param name="lon">The longitude</param>
353private void GetLatLonFromTileXY(int x,int y,int zoom,out double lat,out double lon)
354{
355lon=((x/Math.Pow(2.0,zoom)*360.0)-180.0);
356double n=Math.PI-((2.0*Math.PI*y)/Math.Pow(2.0,zoom));
357lat=(180.0/Math.PI*Math.Atan(Math.Sinh(n)));
358}
359
360/// <summary>
361/// Get the latitude and longitude of a given position in the client
362/// area. This can only work when a view has been drawn. It relies on
363/// information saved during the paint process.
364/// </summary>
365/// <param name="x">The X position</param>
366/// <param name="y">The Y position</param>
367/// <param name="lat">The latitude</param>
368/// <param name="lon">The longitude</param>
369private void ClientPosToLatLon(int x,int y,out double lat,out double lon)
370{
371if(mViewTileSizePixels==0)
372{
373// We can't calculate it!
374lat=lon=0;
375return;
376}
377
378lon=mLon+mScale*(x-Width/2);
379
380double offset=(double)(y-mViewYTileYPos)/mViewTileSizePixels;
381double ty=mViewYTileNum+offset;
382double n=Math.PI-((2.0*Math.PI*ty)/Math.Pow(2.0,mViewTileZoom));
383lat=(180.0/Math.PI*Math.Atan(Math.Sinh(n)));
384}
385
386/// <summary>
387/// Get the view position in the client area of the given latitude and
388/// longitude. This can only work when a view has been drawn. It relies
389/// on information saved during the paint process.
390/// </summary>
391/// <param name="lat">The latitude</param>
392/// <param name="lon">The longitude</param>
393/// <param name="x">The X position</param>
394/// <param name="y">The Y position</param>
395private void LatLonToClientPos(double lat,double lon,out int x,out int y)
396{
397x=Width/2+(int)((lon-mLon)/mScale);
398double ty=((1.0-Math.Log(Math.Tan(lat*Math.PI/180.0)+
3991.0/Math.Cos(lat*Math.PI/180.0))/Math.PI)/2.0*Math.Pow(2.0,mViewTileZoom));
400y=mViewYTileYPos+(int)((ty-mViewYTileNum)*mViewTileSizePixels);
401}
402
403/// <summary>
404/// These are stored during the paint process to be used when
405/// calculating latitude.
406/// </summary>
407private int mViewYTileNum=0;
408private int mViewYTileYPos=0;
409private int mViewTileSizePixels=0;
410private int mViewTileZoom=0;
411
412protected override void OnPaint(PaintEventArgs e)
413{
414base.OnPaint(e);
415
416if(!DesignMode)
417{
418try
419{
420
421// Figure out what tile zoom we want...
422// We want this many degrees longitude per tile...
423double lonpertilewanted=256*mScale;
424// Meaning we want this many tiles across in total.
425int tilesnumwanted=(int)(360.0/lonpertilewanted);
426// Which corresponds to this zoom level...
427int tilezoom=(int)(Math.Log(tilesnumwanted)/Math.Log(2));
428if(tilezoom>18)
429tilezoom=18;
430mViewTileZoom=tilezoom;
431
432// Get the tile x/y at the centre of the view...
433int x,y;
434GetTileXYFromLatLon(mLat,mLon,tilezoom,out x,out y);
435mViewYTileNum=y;
436
437// Get the lat lon at the top left corner of that tile...
438double lat,lon;
439GetLatLonFromTileXY(x,y,tilezoom,out lat,out lon);
440
441// Get the lat lon at the bottom left corner as well...
442double lat2,lon2;
443GetLatLonFromTileXY(x+1,y+1,tilezoom,out lat2,out lon2);
444
445// Calculate longitude degrees per tile...
446double lonpertile=360.0/(Math.Pow(2,tilezoom));
447
448// Calculate longitude degrees per pixel on the tile...
449double lonpertilepixel=lonpertile/256;
450
451// Calculate the size (pixels) that tiles will be scaled to
452// when drawn...
453int tilescalesize=(int)(256*lonpertilepixel/mScale);
454mViewTileSizePixels=tilescalesize;
455
456// Find where to draw the centre tile...
457int drawx,drawy;
458drawx=(int)(Width/2-((double)tilescalesize*((mLon-lon)/(lon2-lon))));
459drawy=(int)(Height/2-((double)tilescalesize*((mLat-lat)/(lat2-lat))));
460mViewYTileYPos=drawy;
461
462// Move the position back to the top-leftmost tile we need to
463// draw...
464while(drawx>=0)
465{
466drawx-=tilescalesize;
467x--;
468}
469while(drawy>=0)
470{
471drawy-=tilescalesize;
472y--;
473}
474
475// Draw all the tiles...
476int curdrawx;
477int curx;
478while(drawy<Height)
479{
480curdrawx=drawx;
481curx=x;
482while(curdrawx<Width)
483{
484Image img=mTileManager.GetTile(curx,y,tilezoom);
485if(img!=null)
486e.Graphics.DrawImage(img,curdrawx,drawy,
487tilescalesize+1,tilescalesize+1);
488curdrawx+=tilescalesize;
489curx++;
490}
491drawy+=tilescalesize;
492y++;
493}
494
495// Draw markers...
496foreach(Marker m in mMarkers)
497{
498int mx,my;
499LatLonToClientPos(m.lat,m.lon,out mx,out my);
500Pen pen=new Pen(Color.Red);
501e.Graphics.DrawEllipse(pen,mx,my,5,5);
502pen.Dispose();
503}
504}
505catch(Exception ex)
506{
507Diags("Drawing Exception: "+ex.Message);
508}
509
510}
511
512}
513
514/// <summary>
515/// Disposes resources used by the form.
516/// </summary>
517/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
518protected override void Dispose(bool disposing)
519{
520if(disposing)
521{
522if(components!=null)
523components.Dispose();
524if(mTileManager!=null)
525mTileManager.NewDataAvailable+=NewTileDataAvailable;
526}
527base.Dispose(disposing);
528}
529
530
531void Timer1Tick(object sender, EventArgs e)
532{
533if(mUpdateView)
534{
535Invalidate();
536mUpdateView=false;
537}
538}
539
540private bool mMouseMoving=false;
541private int mMouseMovingFromX;
542private int mMouseMovingFromY;
543private int mMouseMovingStartX;
544private int mMouseMovingStartY;
545
546void MapControlMouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
547{
548mMouseMoving=true;
549mMouseMovingFromX=e.X;
550mMouseMovingFromY=e.Y;
551mMouseMovingStartX=e.X;
552mMouseMovingStartY=e.Y;
553}
554
555void MapControlMouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
556{
557mMouseMoving=false;
558if(e.X==mMouseMovingStartX && e.Y==mMouseMovingStartY)
559{
560if(LocationClicked!=null)
561{
562double lat,lon;
563ClientPosToLatLon(e.X,e.Y,out lat,out lon);
564LocationClicked(lat,lon);
565}
566}
567}
568
569void MapControlMouseLeave(object sender, System.EventArgs e)
570{
571mMouseMoving=false;
572}
573
574void MapControlMouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
575{
576if(mMouseMoving)
577{
578int movedx=e.X-mMouseMovingFromX;
579int movedy=e.Y-mMouseMovingFromY;
580if(movedx!=0 || movedy!=0)
581{
582mLat+=mScale*movedy;
583mLon-=mScale*movedx;
584Invalidate();
585RaiseViewChanged();
586}
587mMouseMovingFromX=e.X;
588mMouseMovingFromY=e.Y;
589}
590}
591
592}
593}
594

Archive Download this file

Branches