ThinkGeo.com    |     Documentation    |     Premium Support

Getting Clicked Feature - Multiple layers

I have several layers on my map. I would like the user to just be able to click on the map and be able to find the feature that is closest to where the user clicked regardless of what layer it's on. Is this possible?


Currently, I have it set that the user selects the layer from a tree and then clicks on the map and that's how I determine which layer to search on. This is clunky though and I'd like to give the user a better experience than that. Part of the issue is that I have several trees that I'm showing on a tab control so to click on a layer, you have to specifically go to that tab and click on the tree. The first version only had one tree so this was better.


If the above is not possible, then I'd be interested to hear how other developers are providing this functionality for their users. Any really elegant solutions for this? I thought of just providing a selection drop down at the top of the map form (so it's always visible) and letting the user pick from that. Before I take the time to change this, I'm interested to hear other thoughts!


Thanks in advance!


Kimberly



Kimberly, 
  
 As you said, we do not support a built in SpatialQuery based on the MapControl because the SpatialQuery are only supported in the FeatureLayer instead of any kinds of layers. 
  
 While, I think you could write your own logic for this, following is basically my steps: 
 1)Add all the layers you want to do query to one overlay. 
 2)In the MapClick event, loop each layer to see if it is FeatureLayer instead of Finding one specified FeatureLayer. 
 3) Do SpatialQuery on each FeatureLayer and control the results to what you want. 
  
 Any more questions just feel free to let me know. 
  
 Thanks. 
  
 Yale 


Kimberly, 
  
   I am working on a sample now to share with you.  It would be pretty cool to have this as a built in API however there are some caveats.  The main one is that we can only know overlays that inherit from LayerOverlay and from there we only know layers that inherit from FeatureLayer.  If you create your own overlays or layers then we are in the dark. :-( 
  
 The sample I am whipping up will cover the cases above and you can customize it to fit.  I think we will also put this out there as a code community project as well when it is cleaned up. 
  
 David

Kimberly,



  I have some sample code for you.  The code will go through all of the overlays and then look for layer overlays and under that look for feature layers.  I coded two ways of doing this, one with nearest neighbor and one with a tolerance in pixels using the GetRecordsInBoundingBox.  The nearest neighbor is slower so I do not recommend that one however it is the only choice if you really need the closes record and the record is very far away from where the user clicked.  Most of the time a tolerance in pixels is pretty good and faster anyway.  



  The methods are pretty self explanatory.  I included the code to call them as an example.  Just past it in the Desktop Edition sample get feature a user clicked on or something like that in the Getting Started tab.  I also get a list of many candidates and near the end of the methods I get the real closest one.  This can be fairly slow as we have to inspect every vertex and if you have complex polygons then it might not be good.  You could skip that part and just go the first results which might be a few records, but they will all be close.



  The reason this is hard to build in by default is that there are many caveats.  First what kind of search you want, nearest enighbor etc.  The next is do you really want it to search all layers, I mean are you sure?  What if you only wanted it to search a few then how would that look.  Also it only handles layer and overlay types we know.  If you inherit from LayerOverlay or FeatureLayer then you are ok but if you have your own custom layers then there is no way for us to know how to query them.  It also doesn't tell you what layer or overlay they came from, this could be very critical and we could enhance the code to do this.  It could pass back a structure including the overlay and layer along with the feature.  This might be more handy.  Many times after you get it back you want to highlight it etc and if the API only got the closest record is that enough?    I think there is some good ideas in putting this as a standard map API but we need to think about all the scenerios and not rush to put in something well thought out.



  Anyway these are just some of my random thoughts about it.  i agree we need something in there for the 80% of cases and for the 20% they need to write code.  I think along with this method we need something like a HighlightInteractiveOverlay that has some highlighting stuff built in to make the process really easy.  Anyway here is the code.



David




        void winformsMap1_MapClick(object sender, MapClickWinformsMapEventArgs e)
        {
            Feature featureByTolerance = GetNearestFeatureByTolerance(winformsMap1, new ScreenPointF(e.ScreenX, e.ScreenY), 3);
            Feature featureNearest = GetNearestFeatureNearestNeighbor(winformsMap1, new ScreenPointF(e.ScreenX, e.ScreenY));

            if (featureByTolerance.ColumnValues != null)
            {
                MessageBox.Show(featureByTolerance.ColumnValues["cntry_name"].ToString(), "By Tolerance", MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1, (MessageBoxOptions)0);
            }

            if (featureNearest.ColumnValues != null)
            {
                MessageBox.Show(featureNearest.ColumnValues["cntry_name"].ToString(), "Nearest", MessageBoxButtons.OK, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1, (MessageBoxOptions)0);
            }
        }

        private Feature GetNearestFeatureNearestNeighbor(WinformsMap map, ScreenPointF screenSearchPoint)
        {
            Collection<Feature> features = new Collection<Feature>();

            // Convert the screen point to a world point
            PointShape worldSearchPoint = ExtentHelper.ToWorldCoordinate(map.CurrentExtent, screenSearchPoint, map.Width, map.Height);

            // Loop through the overlays.  
            // If you use static and dynamic you would need to change this code a bit
            foreach (Overlay overlay in map.Overlays)
            {
                // Make sure the overlay is a LayerOverlay.                
                if (overlay is LayerOverlay)
                {
                    foreach (Layer layer in ((LayerOverlay)overlay).Layers)
                    {
                        // Make sure the layer in the overlay is a FeatureLayer so we can 
                        // do a spatial query
                        if (layer is FeatureLayer)
                        {
                            FeatureLayer featureLayer = (FeatureLayer)layer;

                            // Use the GetFeaturesNearestTo.  This method has some good and bad sides to it.
                            //  It will get the closest record but if the record is far away it might be slow.
                            // Also this may not return back the closest record 100% of the time but will return
                            // very close records.  Making the return number (10) higher will be slower but more accurate
                            Collection<Feature> nearestFeatures = featureLayer.QueryTools.GetFeaturesNearestTo(worldSearchPoint, map.MapUnit, 10, ReturningColumnsType.AllColumns);
                            foreach(Feature feature in nearestFeatures)
                            {
                                //  Add the result of that layer to a collection of possible canidates
                                features.Add(feature);
                            }
                        }
                    }
                }
            }

            // Call this method that will find the real closest feature
            // If you remove this line you could have a group of the closest features
            // instead of just one
            Feature closestFeature = GetClosestFeature(features, worldSearchPoint, map.MapUnit);

            return closestFeature;
        }

        private Feature GetNearestFeatureByTolerance(WinformsMap map, ScreenPointF screenSearchPoint, int pixelTolerance)
        {
            Collection<Feature> features = new Collection<Feature>();

            // Find out how many meters are in a pixel
            double pixelDistanceInMeters = ExtentHelper.GetWorldDistanceBetweenTwoScreenPoints(map.CurrentExtent, screenSearchPoint, new ScreenPointF(screenSearchPoint.X - 1, screenSearchPoint.Y - 1), map.Width, map.Height, map.MapUnit, DistanceUnit.Meter);

            // Convert the screen point to a world point
            PointShape worldSearchPoint = ExtentHelper.ToWorldCoordinate(map.CurrentExtent, screenSearchPoint, map.Width, map.Height);

            // Create a search rectangle using the pixel tolerance
            RectangleShape searchRectangle = new EllipseShape(worldSearchPoint, pixelDistanceInMeters * pixelTolerance, map.MapUnit, DistanceUnit.Meter).GetBoundingBox();

            // Loop through the overlays.  
            // If you use static and dynamic you would need to change this code a bit
            foreach (Overlay overlay in map.Overlays)
            {
                // Make sure the overlay is a LayerOverlay.    
                if (overlay is LayerOverlay)
                {
                    foreach (Layer layer in ((LayerOverlay)overlay).Layers)
                    {
                        // Make sure the layer in the overlay is a FeatureLayer so we can 
                        // do a spatial query
                        if (layer is FeatureLayer)
                        {
                            FeatureLayer featureLayer = (FeatureLayer)layer;

                            // Here we find the records that are within the tolerance using
                            // the fast GetFeaturesInsideBoundingBox.  This is going to be faster
                            // that the nearest neighbor but will not find items far away even if it is the closest
                            Collection<Feature> nearestFeatures = featureLayer.QueryTools.GetFeaturesInsideBoundingBox(searchRectangle, ReturningColumnsType.AllColumns);
                            foreach (Feature feature in nearestFeatures)
                            {
                                features.Add(feature);
                            }
                        }
                    }
                }
            }

            // Call this method that will find the real closest feature
            // If you remove this line you could have a group of the closest features
            // instead of just one
            Feature closestFeature = GetClosestFeature(features, worldSearchPoint, map.MapUnit);

            return closestFeature;
        }

        private Feature GetClosestFeature(Collection<Feature> features, PointShape worldSearchPoint, GeographyUnit mapUnit)
        {
            Feature closestFeature = new Feature();

            // This there is only one feature then just use it
            if (features.Count == 1)
            {
                closestFeature = features[0];
            }
            else
            {
                double shortestDistance = double.MaxValue;

                // Loop through all of the features look for the shortest distance
                foreach (Feature feature in features)
                {
                    double distance = feature.GetShape().GetDistanceTo(worldSearchPoint, mapUnit, DistanceUnit.Meter);
                    if (distance < shortestDistance)
                    {
                        shortestDistance = distance;
                        closestFeature = feature;
                    }
                }
            }

            return closestFeature;
        }
 


 



2 Likes