ThinkGeo.com    |     Documentation    |     Premium Support

Catch "finished refresh" event in WPF Map Control

Hi,



I need to created a map bitmap after the map refreshing is finished.



I’ve tried to use a code sample which I found in the thinkgeo.com/forums/MapSuite/tabid/143/aft/11319/Default.aspx forum post.



I use the Map Control OverlayDrawn event which is raised for each Map1.Overlays overlay.

My code creates a Map bitmap file (Map1.GetBitmap(width, height)) when all overlays are already drawn, but it looks like the Drawn event is fired before the map refreshing is completed.



The following is my Map1_OverlayDrawn code:



       private void Map1_OverlayDrawn(object sender, OverlayDrawnWpfMapEventArgs e)
        {
            if (_wpfOSMOverlay == null || _dynamicOverlay == null) return;



            if (e.Overlay.Name == _wpfOSMOverlay.Name)
            {
                drawnLayerCount++;
            }
            else if (e.Overlay.Name == _dynamicOverlay.Name)
            {
                drawnLayerCount++;
            }



            if (drawnLayerCount == 2)
            {
                this.OnMapDrawIsFinished(); // A Bitmap is created on this event
                drawnLayerCount = 0;
            }
        }




Could you please let me know if there is a way to catch a “map refreshed” event?



Thanks,

Gene






Hi Gene,



Wpf Edition is using multi-thread to render layers and it is hard to catch overlay drawn event, but we have a workaround with the DrawTilesProgressChanged event, please check the below codes:




overlay.DrawTilesProgressChanged += staticOverlay_DrawTilesProgressChanged;
 
void staticOverlay_DrawTilesProgressChanged(object sender, DrawTilesProgressChangedTileOverlayEventArgs e)
        {
            TileOverlay currentTileOverlay = (TileOverlay)sender;
 
            //only those overlays that are in MultiTile mode and visible takes credit
            var tileOverlays = wpfMap1.Overlays
                                .OfType<TileOverlay>()
                                .Where((to => to.TileType != TileType.SingleTile && to.IsVisible));
 
            int total = tileOverlays.Count() * 100;
            int progress = 0;
            foreach (TileOverlay tileOverlay in tileOverlays)
            {
                LayerOverlay layerOverlay = tileOverlay as LayerOverlay;
                if (layerOverlay != null && layerOverlay.Layers.Count == 0)
                {
                    progress += 100;
                }
                else
                {
                    progress += GetDrawingProgress(tileOverlay);
                }
            }
 
            if (progress >= 100)
            
                Bitmap bitmap = wpfMap1.GetBitmap();
            }
        }
 
        private static int GetDrawingProgress(TileOverlay tileOverlay)
        {
            Canvas drawingCanvas = ((Canvas)tileOverlay.OverlayCanvas.Children[0]);
            int total = drawingCanvas.Children.Count;
            int drawn = 0;
            foreach (ThinkGeo.MapSuite.WpfDesktopEdition.Tile tile in drawingCanvas.Children)
            {
                if (tile.IsOpened) drawn++;
            }
 
            if (total != 0)
            {
                return drawn * 100 / total;
            }
            else
            {
                return 0;
            }
        }

Please feel free to let us know if you have questions on how to use it.



Thanks,



Troy

Hi Troy, 



I’ve implemented your code to my project, but unfortunately it has the same issue. The bitmap is created before the map refresh is completed. 



I’ve added an extra line to save the bitmap to a file (below). The staticOverlay_DrawTilesProgressChanged code creates more than one file and none of them has the final map view.                   



… 

Bitmap bitmap = wpfMap1.GetBitmap(width, height); 

bitmap.Save(@“c:\temp\map” + DateTime.Now.Second.ToString() + “.png”); 

… 



In the most cases, my map reuses already generated tiles, but I don’t think this is the issue. I understand that it may be no solution for this multi-thread scenario, but please let me know if there is a way around. 



Thank you very much for your help, 

Gene

Hi Gene, 



Please try the following code to work it around. 



<thinkgeo.mapsuite.wpfdesktopedition.tile><t><t><t><t>


           wpfMap1.LayoutUpdated += wpfMap1_LayoutUpdated;
       bool isSaved = false;
       void wpfMap1_LayoutUpdated(object sender, EventArgs e)
       {
           if (isSaved) { return; }
 
          
           var tiles = FindChildren<ThinkGeo.MapSuite.WpfDesktopEdition.Tile>(wpfMap1);
 
           if (tiles.Any(p => !p.IsLoaded))
           {
               return;
           }
           isSaved = true;
           var bitmap = wpfMap1.GetBitmap((int)wpfMap1.ActualWidth, (int)wpfMap1.ActualHeight);
           bitmap.Save("d:\" + Guid.NewGuid().ToString("N") + ".jpg");
       }
 
       private List<T> FindChildren<T>(DependencyObject parent) where T : DependencyObject
       {
           var children = new List<T>();
 
           var childrenCount = VisualTreeHelper.GetChildrenCount(parent);
           for (int i = 0; i < childrenCount; i++) 
           {
               var child VisualTreeHelper.GetChild(parent, i);
               if (child is T)
               {
                   children.Add((T)child);
               }
               else 
               {
                   if (child != DependencyProperty.UnsetValue && VisualTreeHelper.GetChildrenCount(child) > 0)
                   {
                       children.AddRange(FindChildren<T>(child));
                   }
               }
           }





 NOTE: 

The layoutupdated event will be fired multi times.  



Thanks, 

Peter</t></t></t></t></thinkgeo.mapsuite.wpfdesktopedition.tile>

Hi Peter,



Sorry for the delay, I’ve tested this code today, but it does not work as expected. The image is still saved before the map is refreshed.



I’ve attached two images. The first jpg file was created in your code and the second png file was created for the same map on a button click.



Please let me know if there is another way to catch the map refreshed event.



Thanks,

Gene









Hi Gene, 
  
 Peter’s code is work well for worldmapkit + shapefile. So I think if that don’t works maybe related with what you are using. 
  
 Could you please let us know your data type or a simple sample should be helpful. 
  
 Regards, 
  
 Don

Hi Gene,



I created a sample to show how save a map after the drawing programmatic, please check it. The below has some key codes below:


private void WpfMap_Loaded(object sender, RoutedEventArgs e)
        {
            wpfMap1.MapUnit = GeographyUnit.DecimalDegree;
            WorldMapKitWmsWpfOverlay worldMapKitOverlay = new WorldMapKitWmsWpfOverlay();
            wpfMap1.Overlays.Add(worldMapKitOverlay);
 
            ShapeFileFeatureLayer worldLayer = new ShapeFileFeatureLayer(@"…\Data\Countries02.shp");
            worldLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
            worldLayer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle = AreaStyles.CreateSimpleAreaStyle(GeoColor.SimpleColors.Transparent, GeoColor.FromArgb(100, GeoColor.SimpleColors.Green));
 
            LayerOverlay staticOverlay = new LayerOverlay();
            staticOverlay.Layers.Add(“WorldLayer”, worldLayer);
            staticOverlay.TileType = TileType.SingleTile;
            wpfMap1.Overlays.Add(staticOverlay);
 
            wpfMap1.CurrentExtent = new RectangleShape(-133.2515625, 89.2484375, 126.9046875, -88.290625);
 
            foreach (Overlay overlay in wpfMap1.Overlays.OfType<TileOverlay>()
                                .Where((to => to.TileType != TileType.SingleTile && to.IsVisible)))
            {
                TileOverlay tileOverlay = overlay as TileOverlay;
                tileOverlay.DrawTilesProgressChanged += new System.EventHandler<DrawTilesProgressChangedTileOverlayEventArgs>(tileOverlay_DrawTilesProgressChanged);
            }
 
            wpfMap1.Refresh();
        }
 
        void tileOverlay_DrawTilesProgressChanged(object sender, DrawTilesProgressChangedTileOverlayEventArgs e)
        {
            var tileOverlays = wpfMap1.Overlays
                                .OfType<TileOverlay>()
                                .Where((to => to.TileType != TileType.SingleTile && to.IsVisible));
 
            bool isFinished = tileOverlays.All(o => CheckIsDrawn(o));
 
            if (timer == null)
            {
                timer = new DispatcherTimer();
                timer.Interval = TimeSpan.FromSeconds(1);
                timer.Tick += (s, a) =>
                {
                    timer.Stop();
 
                    string id = Guid.NewGuid().ToString();
                    System.Drawing.Bitmap map = wpfMap1.GetBitmap(1000, 800);
                    map.Save(“d:\” + id + “.png”);
                    Process.Start(“d:\” + id + “.png”);
                };
            }
 
            if (isFinished)
            {
                timer.Stop();
                timer.Start();
            }
        }
 
        private static bool CheckIsDrawn(TileOverlay tileOverlay)
        {
            bool isFinished = true;
            Canvas drawingCanvas = ((Canvas)tileOverlay.OverlayCanvas.Children[0]);
            if (drawingCanvas.Children.Count != 0)
            {
                foreach (ThinkGeo.MapSuite.WpfDesktopEdition.Tile tile in drawingCanvas.Children)
                {
                    if (!tile.IsOpened)
                    {
                        isFinished = false;
                        break;
                    }
                }
            }
            else isFinished = false;
 
            return isFinished;
        }

Thanks,




 Troy

004_003_002_001_DisplayASimpleMapForWPF.zip (1.5 MB)


Hi Troy,



Thank you for your help and the code sample.
This approach works better than the previous one, but it still does not work as expected for our map. 



Our main Map overlay has OsmWorldMapKitLayer from an OSM SQLite database and three Geo TIFF layers.
The second Map overlay has a few dynamic in memory layers (InMemoryFeatureLayer). 
<LayerOverlay _dynamicOverlay = new LayerOverlay();  _dynamicOverlay.IsBase = false;>.
Both overlays are in the multi-tile mode.



I try to get a bitmap after changing a map extent. With your code, the bitmap file has a correct view of the main layers, but in the most cases, the bitmap is created before in memory layers drawing is completed or even started.



I have a couple questions regarding your code. 
Why the bitmap creation step is when the timer stops, not when isFinished = true? Where should I reset timer back to null? In your sample a bitmap can be created only once, because timer is not null after the first bitmap. When I tried to set timer = null between creation two bitmaps for two different map extent, the code fails with a null exception error.



Any ideas?



I’ll do some more testing on Monday.




Thanks,
Gene

















Hi Gene,



For the reason why dynamic layers don’t be drawn, would you please verify if caused by condition "to.TileType != TileType.SingleTile && "? I think you can try to remove it. In my previous sample, it is only used to catch WorldMapKitWmsWpfOverlay no include the dynamic layers.



As for the timer, it is hard to explain and we can’t set the timer as null. Actually, in the snippet code:

if (isFinished)
            {
                timer.Stop();
                timer.Start();
            }

the above code will put the timer off one second to make sure last tileoverlay is finished. 



Please let us know if any questions.

Thanks,

Troy

Hi Troy,



1. I’ve removed the to.TileType != TileType.SingleTile validation, but it does not help.



2. It is not critical to know why we cannot reset the timer to null, but in your sample, a bitmap can be created only once, because timer is not null after the first bitmap. 



3. Why the bitmap creation step is when the timer stops, not when isFinished = true?


I’ve added the save bitmap step to both places (see below). The “A1.png” and “A2.png” images have the same problem. If the code creates one more image for a different map extent, then only one “A2” image is created because  timer is null only for the first image.





           if (timer == null)
            {
                timer = new DispatcherTimer();
                timer.Interval = TimeSpan.FromSeconds(1);
                timer.Tick += (s, a) =>
                {
                    timer.Stop();



                    string id = Guid.NewGuid().ToString();
                    System.Drawing.Bitmap map = GetMapImage();            //Map1.GetBitmap(1000, 800);
                    map.Save(“C:\Temp\” + id + “A2.png”);
            }



            if (isFinished)
            {
                timer.Stop();
                timer.Start();
                
                string id = Guid.NewGuid().ToString();
                System.Drawing.Bitmap map = GetMapImage();            //Map1.GetBitmap(1000, 800);
                map.Save(“C:\Temp\” + id + “A1.png”)
            }



Please help,

Gene



P.S. I’ve spent a few hours testing/adjusting this code today. I’ve also tested it on another computer. I’m getting different images for the same map extent and sometime the main overlay is not refreshed and sometimes the in memory layer overlay is not refreshed.

Hi Gene,



would you mind to send your application source codes to us via email or upload it to our FTP if it is large? For more details on how to upload sample, please refer to wiki.thinkgeo.com/wiki/map_s…to_support 



We would like to have a further look into the issues and provide a solution after received it.



Thanks,

Troy

Hi Troy, 
  
 It will be very difficult to provide you with my VS project to test on your side. It has a lot of code specific to our tool and data. Our Map Control layers are connected to a 32GB SQLite database with updated OSM data and our tables. 
  
 I may try to reproduce this issue on one of your code samples, but it could take time. 
  
 But in any case, your code allows to create a bitmap for one map only. Could you please answer my questions in the previous post? 
  
 Thanks, 
 Gene

Hi Gene,



For the #1 issue, I didn’t recreate the issue so I have no idea why some overlays didn’t be drawn, but would you please try to set all the Overlay as SingleTile?



The reason we don’t put the save bitmap operation in IsFinished condition is  this condition may trigger multi time and only the last time is a real finished. So, we use a timer as a strategy to catch the last time “finish”.



For the map only be drawn only one time, I think you can set a flag to represent if the map need to save, then every time when we want to save the map, we just need to change the flag status. For example, we can mark it as true to draw when when the MapExtentchanged event is triggered, as following:



void wpfMap1_CurrentExtentChanged(object sender, CurrentExtentChangedWpfMapEventArgs e)
        {
            shouldSave = true;
        }
void tileOverlay_DrawTilesProgressChanged(object sender, DrawTilesProgressChangedTileOverlayEventArgs e)
        {
            var tileOverlays = wpfMap1.Overlays
                                .OfType<tileoverlay>()</tileoverlay>
                                .Where((to => to.IsVisible));



            bool isFinished = tileOverlays.All(o => CheckIsDrawn(o));



            if (timer == null)
            {
                timer = new DispatcherTimer();
                timer.Interval = TimeSpan.FromSeconds(1);
                timer.Tick += (s, a) =>
                {
                    timer.Stop();



                    if (shouldSave)
                    {
                        string id = Guid.NewGuid().ToString();
                        System.Drawing.Bitmap map = wpfMap1.GetBitmap(1000, 800);
                        map.Save(“d:\A” + id + “.png”);
                        Process.Start(“d:\A” + id + “.png”);
                        shouldSave = false;
                    }
                };
            }



            if (isFinished)
            {
                timer.Stop();
                timer.Start();
            }
        }



The above code will save a bitmap after the extent changed.



I try to mock up your case, in my test sample, I add a 140G sqlite database and a shape file layer, but the sample works as my expected. Please see my test video. screencast.com/t/0zUZILfXJH 



Also, we expect your sample.



Thanks,



Troy




Hi Troy, 
  
 I’m using a similar shouldSave flag and I’ve also tried to use the SingleTile mode. Unfortunately it did not help.    
  
 I’ve reviewed your code and the video but still don’t understand how you can save multiple images if you check that shouldSave == true after checking  if (timer == null). The timer will always be not null after the first image is saved. 
  
 I try to reproduce my issue in the World_Map_Kit_SDK_EditionSample_OsmWorldMapKitExplorer_CS_150917 sample but so far the sample code does not work with my SQLite database.  
  
 Do you think I could use your code sample?  
  
 Thanks, 
 Gene 
  


Hi Gene,



Sure, please download the attached sample and try to recreate your issues based on it.



Thanks,



Troy

005_004_003_002_001_DisplayASimpleMapForWPF.zip (1.64 MB)

Troy,



I have a generic issue with the WPF Map Control when I try to use your code or when I try to create a new test project from scratch. 



My main code does not have this problem and I’m using the same ThinkGeo DLL package for the main and test projects.



I’m getting a generic "System.InvalidOperationException was unhandled, HResult=-2146233079, Message=The caller assembly doesn’t match"
error on the wpfMap1.Refresh(); code.


I’ve spent a lot of time without any success. Project’s platform is set to “All CPU”. It looks like a problem with the VS 2010 C# project settings or references. I’ve tried many different settings. I’ve removed links to SQLite, I’ve even try to set the wpfMap1.BackgroundOverlay.BackgroundBrush only without a shapefile layer and got the same error.




The following is a very simple code which raises this error:



private void WpfMap_Loaded(object sender, RoutedEventArgs e)
{
try
{
wpfMap1.MapUnit = GeographyUnit.Meter;



ShapeFileFeatureLayer worldLayer = new ShapeFileFeatureLayer(@“C:\Projects\IntelliRoute\ShapeFileTest\ne_state5m_polygon.shp”);
worldLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
worldLayer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle = AreaStyles.CreateSimpleAreaStyle(GeoColor.SimpleColors.Transparent, GeoColor.FromArgb(100, GeoColor.SimpleColors.Green));



LayerOverlay dynamicOverlay = new LayerOverlay();
dynamicOverlay.Layers.Add(“WorldLayer”, worldLayer);
dynamicOverlay.TileType = TileType.SingleTile;
wpfMap1.Overlays.Add(dynamicOverlay);
worldLayer.Open();
wpfMap1.CurrentExtent = worldLayer.GetBoundingBox();  

wpfMap1.Refresh();



}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}




Again the same set of ThinkGeo dlls work with my main project.

Any idea? Please help!



Thanks,

Gene

Hi Gene, 
  
 I can’t reproduce your issue. Can you send your whole project to me(contains references ThinkGeo DLL packages)?  
  
 Thanks,