ThinkGeo.com    |     Documentation    |     Premium Support

Map Clustering Xamarin Forms to MAUI migration questions

Hello ThinkGeo,

I am seeking some help with my migration work.
I am migrating a fairly large XF app that had a XF Google Maps implementation.

In migrating from XF to MAUI I am also moving from the Google Maps implementation over to ThinkGeo.

Our use of the map is quite intense. Generally the users focus will not be wider than a City or Town. They will want to focus down to the street level and often multiple points around the City. That means lots of zooming in an out to allow them to move around the City. At those points we will have many hundred features. A typical focus point maybe lets say a housing block or street level. Looking over 20+ dwellings and 100+ features in that area.
Of those features there will be 6+ different types and we would use a PointStyle or the like to give each feature a different style shape and or color.

As you can imagine at some zoom levels you will be presented by a blanket of features and unable to see the map detail underneath them. Yet once zoomed in to a point you will want to see every individual feature.

Clustering is the answer.
Your clustering looks like it comes close to what we need and I have many questions below around your clustering implementation but we did develop our own clustering on the XF Google Maps implementation and I do have some questions around how to migrate that over.

I hope you can help and I hope that I have provided an outline on how we use a map.

Questions around our clustering solution
We implemented our own clustering as the google grid system just did not work the way the users expected it. It just did not make sense to them. We clustered based on a Haversine distance algorithm. NOTE that I have very much a beginners knowledge in this area.

One important element here is obtaining the maps current area so we can calculate the Haversine distance. The Google Maps implementation had properties that gave us the Long and Lat of the "Near Right’ and “Far Right” of the Map “region” currently in view. This does not appear to exist with ThinkGeo or I cannot find a way to obtain those values. Can you help with this?

Questions about your clustering implementation
You cluster at the Layer level. It’s a grid and I think this may work for us.
It appears that your cluster will be a “Gravity Well” in nature of where the Cluster is placed. As in based on the features that are in the cluster the final position of the cluster is calculated.
Is that correct?

I create a clusterstyle and add it to the zoomlevel set of the layer. Pretty much like your sample.
What I would like to do is have one pointstyle for the features not clustered and of course the clusterstyle for the clustered features.
I tried that by applying just that to the zoomlevel set as such
inMemoryFeatureLayer.ZoomLevelSet.ZoomLevel01.CustomStyles.Clear();
inMemoryFeatureLayer.ZoomLevelSet.ZoomLevel01.CustomStyles.Add(pointStyle);
inMemoryFeatureLayer.ZoomLevelSet.ZoomLevel01.CustomStyles.Add(clusterPointStyle);
inMemoryFeatureLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;

This did not work the way I expected. Lets say I have 6 features and based on my clustering settings 3 will be clustered and 3 will not. What I expected was to have on the map was 3 in the pointStyle and 3 in the clusterPointStyle.

What I got was 6 in the pointStyle and 3 in the clusterPointStyle. The clustered features are not removed or hidden from the map display. How can I achieve the desired output of 3 points and 3 clustered?

Is there a way I can tell if a feature has been clustered?

The scenario I actually need is to have my many features that will have one of the 6+ styles applied to them (based on an attribute) but for the features that will be clustered based on my settings to have a different style and not be displayed as individual featuers.

It doesn’t seem that I can achieve that looking at your samples. With clustering all the features on the layer must have the same style with the exception of a number based from the feature count attribute.

Can you help here and clear this up?

As you can see my use of the map is fairly complex and I hope I have explained it above or maybe I just made it harder to understand?

Happy to provide and discuss further.

Regards
Chris …

Oh and another thing I forgot to mention the feature data is live.
Based on API calls features will be:

  • Features added to the map
  • Features removed from the map
  • Features Lat/Long position updated.

The majority (80%) of the features are static in position but the other 20% are live and constantly moving.

Hi Chris,

Thanks for asking!

  1. The ClusterPointStyle maintains a configurable grid matrix, and check the number of features within each grid to decide whether / how to cluster.

  2. The following code will render the points layer twice (with and without the cluster), you in fact don’t need the first line in your scenario.

inMemoryFeatureLayer.ZoomLevelSet.ZoomLevel01.CustomStyles.Add(pointStyle);
inMemoryFeatureLayer.ZoomLevelSet.ZoomLevel01.CustomStyles.Add(clusterPointStyle);
  1. ClusterPointStyle has the following 2 properties for the clustered points and non-cluster points(as well as the clustered points if ClusteredPointStyle is not defined)

ClusterPointStyle.DefaultPointStyle
ClusterPointStyle.ClusteredPointStyle.

I don’t think you have set the ClusteredPointStyle, which we didn’t show in our sample as well, we’ll add it, please have a try and that will let you style the non-clustered point / cluster points differently.

  1. The event DrawingClusteredFeature will be raised before drawing the clustered points. In its event args you can see the features being clustered, and even modify the clustered points and its styles.

  2. Not sure if I understand correctly but you can use mapView.CurrentExtent to get the current view, or use layer.GetBoundingBox() to get the bbox of a given layer.

  3. It’s fine if the data is live, that just means we cannot enable TileCache for the overlay, just need to render on the fly. The performance depends on the number of points being clustered/displayed on the current view.

So I don’t think you were using ThinkGeo component in your XF project, right? Just let us know when you have any questions.

Thanks,
Ben

Ben,

Thanks for the response.
You certainly cleared up the clusterstyle question.
I do have a question about your clustering.

Are you saying this would be the preferred way to set custom styles when clustering?
Any examples of this?

What I am after here is the Lat and Long of the Extent.
Chasing the properties … CurrentExtent.LowerRightPoint.X is not a Lat or Long??? I am not sure what this value is.
With my custom clustering logic, I need to

  1. find the lat/long of the currentextent edges.
  2. I then convert the degrees to radians
  3. I then perform some complex math to obtain the Haversine Distance from the numbers in the step above.
    The custom cluster logic uses that Haversine distance.

I hope this helps you understand why I am looking for the Lat/Long of the CurrentExtent.

You are correct that this is my first time using ThinkGeo in a mobile app.

Regards
Chris …

Hi Chris,

On clustering & styling

  • DrawingClusteredFeature is optional. You only need it when you want to override the default look of a cluster on a per-cluster basis (e.g., change color/size based on the number of features inside the cluster). A simple example:
// if a cluster has >10 items, draw it as a larger red circle.
clusterPointStyle.DrawingClusteredFeature += (s, e) =>
{
    if (e.ClusteringFeatures.Count > 10)
    {
        e.Styles.Clear();
        e.Styles.Add(new PointStyle(PointSymbolType.Circle, 20, GeoBrushes.Red));
    }
};

If you just need one style for single points and another for clusters, set:

  • clusterPointStyle.DefaultPointStyle (for non-clustered features)
  • clusterPointStyle.ClusteredPointStyle (for clustered features)
    and you don’t need the event at all.

On getting the current view in lat/long

mapView.CurrentExtent is in the map’s current projection. If your base map is Web Mercator (Google/Bing style), that’s EPSG:3857 (meters). To get lat/long (WGS84, EPSG:4326), reproject the extent, then read its corners:

// Current extent in whatever the map is using (likely 3857 if you use Web Mercator)
var extent3857 = mapView.CurrentExtent;

// Reproject 3857 -> 4326 (lon/lat)
var extent4326 = ProjectionConverter.Convert(3857, 4326, extent3857);

// Now you have lon/lat corners
var sw = extent4326.LowerLeftPoint;   // sw.X = lon, sw.Y = lat
var ne = extent4326.UpperRightPoint;  // ne.X = lon, ne.Y = lat
// If you prefer the “near/far right/left” wording:
// near-right == LowerRightPoint, far-right == UpperRightPoint in ThinkGeo naming.

you can also use ThinkGeo’s API to directly get the length if that fits in your algorithm.


LineShape lineShape = new LineShape(); 
//lineShape.Vertices.Add(startVertex); 
//lineShape.Vertices.Add(endVertex); 
var length = lineShape.GetLength(3857, DistanceUnit.Meter, DistanceCalculationMode.Haversine);

Thanks,
Ben

Ben,

Thanks for clearing up my question around the DrawingClusteredFetaure event.

Lat/Long of the Extent … thank you for the info and sample code.
I assume those numbers 3857 and 4326 are sonething you just know … but for someone like me … where will this be documented? How would I know what projection I am using?

Now I am playing with porting our clustering texhnique over to work with your Map … otherwise how can I prove that I prefer your clustering technique :slight_smile:
I assume the event CurrentExtentChanged is what I am using. But this fires twice. I cannot see why it fires twice. I am either double tapping on the map surface to zoom or I am using the ZoomMapTool control. Either way it will fire the event twice.

From my rough estimate it seems to fire the first time when 70% zoomed and then fires again once the zoom is 100% complete.
Why is it firing twice?
What can I use to ensure I only run my clustering code on the last/Final zoom event?
Will it always fire twice and I can simply wait for the second fire? (this feels very risky)

Regards
Chris …

Hi Chris,

I just get used to the magic code :slight_smile: You can do the following instead.

ProjectionConverter.Convert(Projection.GetGoogleMapProjString(), Projection.GetLatLongProjString(), extent3857);

Whenever the extent is changed, even during the animation, CurrentExtentChanged will be raised, not just the “last time”. Instead you can use MapView.OverlaysDrawing, which is raised after the animation and before the map drawing.

Thanks,
Ben

Ben,

Projection.Get*****String perfect. I can never forget them.

CurrentExtendChanged … can you elaborate here a bit please.

I would expect when I zoom in that will be a single changed event. Yet the event is fired twice … the extent values (X and Y) are different each time.

So are you saying that I zoom and the steps are:

  1. Extent changed part way.
  2. Animation is done and event fired.
  3. Extent now reaches final point and another event is fired.

OverlaysDrawing … is fired when exactly?

I am looking for a point to capture the extent size … and process my features to perform my custom clustering. So after the user chooses to zoom in or out is when I think is the correct moment as that is what would change my clustering.

ExtentChanged seemed perfect. Changed generally means when complete as Changing could fire repeatedly.
Are you saying that OverlaysDrawing is the better choice to calc the clustering?
I would prefer to perform the clustering only once and not repeatedly as that seems wasteful.

Regards
Chris …

Hi Chris,

When you double-click to zoom, two things happen:

  • Animation – updates the extent/center on the UI thread .
  • Rendering – redraws on a worker thread , then composites the bitmap back.

The animation (170 ms by default) keeps modifying the extent, so ExtentChanged is raised multiple times. You saw it only twice because of breakpoints or slow code inside the handler—normally it’s more.

OverlaysDrawing fires once the animation ends and just before the new image is rendered. Isn’t that a better timing for you? Can you have a quick try?

Thanks,
Ben

Ben,

Mmmm some interesting observations at my end.

OverlaysDrawing does fire once the animation ends. Unfortunately, it also fires when the user performs a pan. There is nothing at all in the event args to help me to distinguish between a zoom or a pan. I don’t need to recalculate and cluster the features if the user hasn’t changed the zoom. To perform that on a pan would make the user experience very poor, I think.

There is an event CurrentScaleChanged which I thought may be suitable but that is never fired.

I am back to the ExtentChanged event.
Thank you for the explanation there.

Possibly a Bug???
I do not have too much experience with .NET so please excuse my ignorance, but I think you may have a bug here with the event firing.
When something is being processed such a zoom I would think that the ExtentChanging to fire repeatedly during the Extent processing. Potentially giving me the opportunity to inspect and cancel or what ever…
But once it has completed all the processing the final event to fire would be ExtentChanged and that would be a single fire as the last and final step.
What do you think? Is my general understanding of Events in .NET and the changing and changed events incorrect?

For now, and moving forward I will go with the ExtentChanged event even though it may fire more than once for the single interaction I can make use of the arg properties IsMapScaledChanged to be true to focus on the right event interaction to choose to cluster.
It also fires on a user pan but the args will help me to ignore those.

While I am trialing our custom clustering solution, I think this is the best event for me to use at the moment.

What are your thoughts?

Regards,
Chris …

Hi Chris,

It’s fine to use ExtentChanged with e.IsMapScaledChanged to detect scale changes. MapView.CurrentScaleChanged does work on my side—could you let me know which version you’re using?

Thanks for your suggestion regarding ExtentChanged . We didn’t implement it that way because it could make the event behavior inconsistent when a zoom is canceled. For example, if a pan starts in the middle of a zoom, ExtentChanged would not fire even though the extent had actually changed. To avoid this ambiguity, we designed it so that ExtentChanging and ExtentChanged are always raised as a pair, leaving developers free to build their own logic on top if needed.

Thanks,
Ben