ThinkGeo.com    |     Documentation    |     Premium Support

Efficient Bitmap Creation from WinformsMap

I have a need to keep an in memory copy of the ThinkGeo WinformsMap, so that I can periodically create a Bitmap object for a portion of the map Overlay.  The idea is to load in a combination of Raster and Feature layers and generate a bitmap that covers some of the resulting image.  Also, if the desired bitmap area is half off of the extent covered by the Overlay, it should be transparent or a solid color.


In the process of figuring out how to do this, I've found a lack of detail in the documentation in the ExtentHelper class.  It would really help a lot of you guys could shore that up a bit so it is very clear what each function argument actually represents.  I also recommend you use a better code documentation tool than NDOC -- I use Doc-O-Matic, which generates PDFs, HTML, CHM, and Windows help files well.


Anyway, suppose we have a class that performs the above functionality, and there are the following member properties:



        
  • int PixelWidth -- width in pixels of output bitmap

  •     
  • int PixelHeight -- width in pixels of output bitmap

  •     
  • RectangleShape rs -- desired 'box' that is put in the bitmap

  •     
  • WinformsMap _Map -- the ThinkGeo map with Width >= PixelWidth & Height >= PixelHeight



public Rectangle UpdateBitmap(RectangleShape rs)
{
   _B = new Bitmap(PixelWidth, PixelHeight);
    if (true)
   {
     // This method is a hack that kind of works, the upper left corner matches rs
     // but it is an inefficient method.
     Rectangle r = new Rectangle(0, 0, PixelWidth, PixelHeight);
     RectangleShape save_rs = _Map.CurrentExtent;
     _Map.CurrentExtent = rs;
     _Map.DrawToBitmap(_B, r);
     _Map.CurrentExtent = save_rs;
   }
   else
   {
     // This is what I tried to do, but could not get it to work
    Rectangle r = ExtentHelper.ToScreenCoordinate(Map.CurrentExtent, rs, PixelWidth, PixelHeight);
   _Map.DrawToBitmap(_B, r);
     // I also tried ExtentHelper.ZoomIntoCenter, but could not get it to work
   }
 }
 


Any help you can provide would be most appreciated.  The goal of this effort is to provide a transition to add the ability to read FeatureLayer sources and integrate it into our current map control as a quick ugprade.  Later, we hope to use your map control instead.  So, this process to generate the bitmap needs to be as efficient as possible.


NOTE: I looked very closely at all of the How Do I? examples, and they do not quite cover this situation.  Any help you can provide would be most appreciated.  It will very much help me in my evaluation process.



 


Brian,
 
After I test your code, I found that _Map.DrawToBitmap(_B, r); will call OnPaint method, and our WinformsMap override this method to make the resize works, it will call WinformsMap.Refresh() inside OnPaint. So maybe the performance low is caused this. I will continue investigate this issue, if we find solution I will let you know.
 
Now, you can try to turn on the TileCache to optimize the performance. You can set InMemoryBitmapTileCache to each overlay.
overlay.TileCache = new InMemoryBitmapTileCache();
 
If I misunderstand your problem, please supply more information about your requirement, the best way is create a small sample attached.
 
Thanks
 
James

I'm not sure you understand my question.


First, some notation.  LL(x, y) is Longitude x and Latitude y.  Now, assume that our in-memory WinformsMap.CurrentExtent goes from LL(-44, 22) to LL(-33, 55) -- lower left to upper right corner of extent.


If I want to return a Bitmap object from LL(-40, 25) to LL(-35, 50) without changing WinformsMap.CurrentExtent, how do I do that the best way?


I want to do this many times on the same overlay as I pan across the shapefile based map.  I know, you are probably thinking, why don't you just use our map, it provides good panning?  I agree with you, but I can not do that yet, because there is a lot of custom code that would need to be modified to make that happen.  I simply want to be able to add shapefile based data in an existing framework for the next few releases of our product.


Does that make sense?  I'm not sure if the tiling stuff helps here or not.  If it does, can you help me with a working example?  It would be most appreciated.  Thanks!



Brian, 
  
   To make sure I understand this.  You basically want to draw an arbitrary extent behind the scenes to a bitmap.  For example you may have the map looking at Japan and when the user clicks a button, let’s say, you want the map to crank out a 600x600 image of the US using the same layers but not changing anything about the map that is currently being displayed on the screen? 
  
 David

Not quite.  To me, whether WinformsMap is actually visible on screen or not is not the issue.


Suppose the _Map.CurrentExtent is the result of loading a RasterLayer covering the continent of Africa.  Also assume that it includes the country boundaries and street network.  Those 3 layers make up the overlay of Africa.  Now suppose that I want to generate a Bitmap of that _Map that covers Sudan that is 300x200 pixels in size -- without touching the _Map object at all (preferably) or causing any kind of redraw (either onscreen or in memory).


Next, assume I want to ask for another bitmap that is 1 degree of longitude to the East of the first Sudan one.  Say we want to repeat many times, but all of the same overlay of Africa.  This is similar to the Minimap, but generating a Bitmap instead.



<overlay><layer></layer></overlay>Brian,



  I have attached some sample code to show you what I think you are looking for.  At least it can be jumping off point.  The sample will draw maps on a bitmap for the overlays you pass in.  Layers are the same thing for the most part and have the same critical Draw method.  You could repace the IEnumerable with IEnumerable and it would work the same.



David



          private void Test()
        {
            Bitmap bitmap = null;

            try
            {
                // Define a bitmap of 640x480
                bitmap = new Bitmap(640, 480);

                // This is going to be my test overlay I am working with
                WorldMapKitWmsDesktopOverlay worldMapKitDesktopOverlay = new WorldMapKitWmsDesktopOverlay();

                // Call the method passing in the extent to draw, the bitmap and the overlay
                DrawMapOnBitmap(bitmap, new RectangleShape(new PointShape(-114, 42), new PointShape(-85, 24)), GeographyUnit.DecimalDegree, new Overlay[] { worldMapKitDesktopOverlay });

                // Save the results to disk so you can check them out
                bitmap.Save(@"C:\ThinkGeoFiles\test.bmp");

            }
            finally
            {
                // dispose of the bitmap 
                if (bitmap != null) { bitmap.Dispose(); }
            }
        }

        private static void DrawMapOnBitmap(Bitmap bitmap, RectangleShape extent, GeographyUnit mapUnit, IEnumerable<Overlay> overlays)
        {
            // create a canvas that uses GDI+.  You could use other canvases that use PDF
            // or just about anything else
            GdiPlusGeoCanvas canvas = new GdiPlusGeoCanvas();
            
            // Adjust the extent so the ratio of the extent matches the ratio of the bitmap.  This is important
            // or you get stretching or compression
            RectangleShape adjustedExtent = ExtentHelper.GetDrawingExtent(extent, bitmap.Width, bitmap.Height);
            
            // Start the canvas drawing system
            canvas.BeginDrawing(bitmap, adjustedExtent, mapUnit);
            
            // Loop through all of the overlays and call their draw method passing in the canvas
            // This could just as easily been down with Layers as well as they also have a draw method
            foreach (Overlay overlay in overlays)
            {
                overlay.Draw(canvas);
            }
            
            // Commit the changes by calling the end drawing
            // At this point the bitmap has been written to
            canvas.EndDrawing();
        }


This is exactly what I was looking for in a function, thank you! 
  
 The adjustedExtent was what I did not have quite right. 
  
 Also, would the InMemoryBitmapTileCache mentioned earlier in this thread be worth doing?  For example, suppose you have a full set of overlays with layers from around the world.  Then you want to zoom in to specific regions (i.e. Africa, Italy, Mexico, etc.).  Would that help with speed?

Brian, 
  
   I’m glad this helped you out.  There is one thing after looking at the code I left out.  In the for each for every overlay there is an Overlay.Draw, after that I think we need to call the Canvas.Flush method.  This will make sure that styles drawn on different levels are consistent across overlays.  It’s tough to explain in a few words but I suggest you add that.  
  
   On the InMemoryBitmapTileCache it all depends…  If the areas that you are showing are typically subsets of larger images then yes it would.  If the area you are showing are about the same size as the images you generate then maybe not.  You can try and play around with it and see if you like it. 
  
 David

Thanks, doing the canvas.Flush() after the o.Draw(canvas) seems to work fine.  However, I have one problem still.  In my class, I have an option to have WinformsMap either in-memory or as a live, visible control.  The intent is that the in-memory function loads a bunch of raster and feature data, then generates a bitmap for the RectangleShape llr of interest that is PixelWidth by PixelHeight pixels in size.


When I load up everthing and generate a bitmap, everything shows up fine both ways (with and without in-memory only).  However, when I set IsVisible to false for one of the raster layers (not the whole overlay), it does not disappear when I generate the bitmap in the in-memory only mode.  However, it does when I have an actual WinformsMap control visible in my GUI.  Note that the code is identical except for one line that either uses the reference to the WinformsMap in the GUI or a _Map = new WinformsMap().


Also of note, the code after the #else does work for this, if you cause a Refresh() on the container (a Panel) that holds the bitmap as its background.




public RectangleShape BitmapUpdate(RectangleShape llr)
{
if (_B == null) _B = new Bitmap(PixelWidth, PixelHeight);
RectangleShape rs = ExtentHelper.GetDrawingExtent(llr, _B.Width, _B.Height);
#if true
GdiPlusGeoCanvas canvas = new GdiPlusGeoCanvas();
canvas.BeginDrawing(_B, rs, _Map.MapUnit);
foreach (LayerOverlay o in _Map.Overlays)
{
o.Draw(canvas);
canvas.Flush(); // Recommended by ThinkGeo to maintain consistency
}
canvas.EndDrawing();
#else
// My old way, but works when a layer.IsVisible = false.
. Rectangle r = new Rectangle(0, 0, PixelWidth, PixelHeight);
RectangleShape save_rs = _Map.CurrentExtent;
_Map.CurrentExtent = llr;
_Map.DrawToBitmap(_B, r);
_Map.CurrentExtent = save_rs;
#endif
return rs;
}


One other thing, when I set llr = new RectangleShape(-180, 90, 180, 0), the Northern Hemisphere, I get a bunch of transparent space above the map.  I assume this has something to do with aspect ratio to bitmap size.  The behavior I want is for the upper left corner of my llr to always match up with the upper left corner of the bitmap _B.  It is OK to have transparency on the bottom, though I'd rather just cut that off by making the bitmap have less Height pixels.



Brian, 
  
  That is very strange.  I looked in the code and the underlying object which is Layer has the concrete Draw method.  In that code it checks if the IsVisible is false and of so it never calls the DrawCore.  I am not sure how anything could get around this.  Can you verify in code right before you call the RasterLayer.Draw that the IsVisible is really false and it is not getting set to try somewhere else?  Also if this is the case could you provide some really small and simple sample of this happening.  I really want to believe you but the code I see says something else.  If course they may be some other variable here that I just haven’t considered. 
  
 David

I've been working on this more today and I did notice that if I set a FeatureLayer to IsVisible = false, then it does disappear, but not RasterLayer items.  Odd.  My method under the #else still works both ways though.


I've tried doing refreshes and pounding it several ways, still without success so far.


My newest challenge is dealing with getting the bitmap up in the left corner all the time.  I assume this can be done with some kind of CenterAt call?  It is usally a problem when the aspect ratio (width/height) of CurrentExtent does not agree with the bitmap pixel aspect ratio.  Making them the same helps a lot, as you can imagine, but I've still not solved it in all cases, because the WinformMap control wants to change the CurrentExtent a bit on its own in some cases... I'll research this more and let you know what I find.



Hi Brian, 
  
 In order to solve your problem, please provide a really sample like David mentioned. 
  
 You don’t need attach the Map Suite DLLs, just let me know the file version of them. And if your sample data is too big to upload, you don’t need attach data too, but you need let me know which kind of data and the required information of it. 
  
 Thanks 
 James