Dealing With Obscene Amounts of Markers

Tags: Tutorial, Google Maps

Dealing with obscene amounts of overlays (typically markers or extremely long and complex polylines) is not recommendable. Most importantly Google's underlying API becomes terribly slow when dealing with more than 200 markers and complex polylines can create odd effects when scrolling the map. But equally important is the impression for the reader. Who can actually absorb the information that lies in 1000 plotted markers?

One of the reasons behind the success of Google's search engine is its ability to present the desired search results at the top of the result list. They are very aware that users don't go through 1000 results and compare and research. In the same way I think it is safe to say it is highly unlikely that users are going to read and/or understand the information hidden behind 1000 markers. This doesn't really supply any useful information.

OK, this is a somewhat artificial example as you may not always be in charge of which search results are returned to the user. But you can be in charge of grouping the returned search results into something useful. Consider for example a real estate site that want to show houses in a certain price range. This would naturally be grouped by town or zip code. To present a friendlier result than throwing all the offers at the user at the same time or obliging him to perform multiple searches the results could be bound to "master markers" representing each zip code.

Google's API has the MarkerManager class which is not implemented in the Google Maps .NET Control. Instead the control has several functions to help with serverside marker management. After all there is a reason that developers have decided not to throw themselves directly into the clientside API. Also, when using serverside overlay management one can interact with a supporting database instead of reading all the overlays at page load.

Let's have a look at a generic example of how to create a map that first gets the relevant zip codes, plots them and uses them to fetch markers for that area when clicked.

Note, this example is not going to work for the free version as it lacks many of the database integration and collection search functions that the example relies on.

First we have to plot all the zip codes that are relevant to the search. To this end we need an icon to distinguish the zip code markers from the house markers. Afterwards the zip code results get plotted with the zip code icon. The following code shows how a page load event can look like:

C#

protected void Page_Load(object sender, EventArgs e)
{
    //Create a zip code icon and a house icon.

    if (!Page.IsPostBack)
    {
        GoogleIcon zipicon = new GoogleIcon(
            "zip",
            "http://www.yourdomain.com/images/zip.png",
            new GoogleSize(20, 20),
            new GooglePoint(10, 10),
            new GooglePoint(10, 10));

        GoogleMap.Icons.Add(zipicon);
        GoogleIcon houseicon = new GoogleIcon(
            "house",
            "http://www.yourdomain.com/images/house.png",
            new GoogleSize(20, 20),
            new GooglePoint(10, 10),
            new GooglePoint(10, 10));

        GoogleMap.Icons.Add(houseicon);

        //Get all relevant zip codes.
        //In this example I presume that you have a view in your database that joins two tables.
        //One table holds the real estate listings and the other holds zip codes and their locations.
        //The two tables are joined by the zipcode field.

        string qryZip = @"SELECT DISTINCT zipcode, ziplatlng FROM vue_RealEstateZip WHERE price BETWEEN @minprice AND @maxprice";

        SqlCommand cmd = new SqlCommand(qryZip, conn);
        cmd.Parameters.AddWithValue("@minprice", 100);
        cmd.Parameters.AddWithValue("@maxprice", 200);
        conn.Open();
        SqlDataReader dtr = cmd.ExecuteReader();

        while (dtr.Read())
        {
            GoogleMarker zipmarker = new GoogleMarker();
            zipmarker.ID = (string)dtr["zipcode"];
            zipmarker.Point = GoogleLatLng.FromSqlLatLng(dtr["ziplatlng"]);
            //If not using the SqlLatLng UDT you may have to create the GoogleLatLng from other columns.
            zipmarker.Options.Icon = GoogleMap.Icons[0];

            //If we don't know the index position of the icon to use we can use the Find method
            //zipmarker.Options.Icon = GoogleMap.Icons.Find(
            //  delegate(GoogleIcon icon)
            //    {
            //        if (icon.ID == "zip") return true;
            //        return false;
            //    }
            //    );

            zipmarker.Options.Title = (string)dtr["zipcode"];
            //Finally we need to make the map react to clicks on the zip code markers.
            zipmarker.ClientSideHandlers.OnClick =
                GoogleMap.CreateMapCallback((string)dtr["zipcode"], false);
            GoogleMap.Overlays.Add(zipmarker);
        }

        dtr.Close();
        conn.Close();
    }
}

The next thing to do is to react to clicks on zip code markers. In the example above we did not use the conventional OverlayClick event to handle clicks on zip code markers. Instead each marker got a clientside handler that initiates a callback to the ExternalCallback event with the zip code as the argument. So it is in this event that we need to get the zip code and plot the corresponding markers.

First of all remember to register the event handler out the if(!Page.IsPostback) block like so:

C#

GoogleMap.ExternalCallback +=
	new ExternalCallbackHandler(GoogleMap_ExternalCallback);

Then comes the actual event handler. In the event handler any existing house markers are removed first. Then the zip code argument is used to search the database and the returned markers are plotted.

C#

void GoogleMap_ExternalCallback(string Argument, ref string MapCommand)
{
	//First remove all markers that use the house icon (house markers)

	foreach (GoogleOverlay ov in GoogleMap.Overlays.FindAll(
		delegate(GoogleOverlay overlay)
		{
			if (overlay is GoogleMarker && ((GoogleMarker)overlay).Options.Icon.ID == "house") return true;
			return false;
		}))
	{
		MapCommand += GoogleMap.RemoveOverlay(ov, true);
	}

	//Next search the database for housing matches in the given zip code.

	string qryHouse = @"SELECT ID, position, description FROM tbl_RealEstate WHERE (zip = @zip) AND (price BETWEEN @minprice AND @maxprice)";

	SqlCommand cmd = new SqlCommand(qryHouse, conn);
	cmd.Parameters.AddWithValue("@minprice", 100);
	cmd.Parameters.AddWithValue("@maxprice", 200);
	cmd.Parameters.AddWithValue("@zip", Argument);
	conn.Open();
	SqlDataReader dtr = cmd.ExecuteReader();

	while (dtr.Read())
	{
		GoogleMarker house = new GoogleMarker();
		house.ID = (string)dtr["ID"];
		house.Point = GoogleLatLng.FromSqlLatLng(dtr["position"]);
		house.ClientSideHandlers.OnClick =
			house.OpenInfoWindowHTML(
			GoogleMap,
			(string)dtr["description"]);

		MapCommand += GoogleMap.AddOverlay(house, true);
	}

	dtr.Close();
	conn.Close();
}

The above example depends very much on database searching. Theoretically all the markers could be loaded into the overlay collection on the initial load and then RenderSelected property could be passed a Predicate that made the map only display markers using the zipicon, something like this:

C#

GoogleMap.Overlays.RenderSelected =
	delegate(GoogleOverlay overlay)
	{
		if (overlay is GoogleMarker && ((GoogleMarker)overlay).Options.Icon.ID == "zip") return true;
		return false;
	};

But this approach would in all likelyhood lead to an enormous amount of data being moved to and from the client because all the marker data that is not displayed is still stored by the control state management. So if you are going to preload an obscene amount of markers (>500) then make sure you test the user experience first.

As written in the beginning the Google Maps .NET Control does not includes support for the clientside MarkerManager, but from version 3.4 it has many methods to search collections and generate specialized callbacks, which allows very detailed overlays handling using serverside code. Combined with callbacks this should provide a flexible and responsive user experience.

If you are going to be presenting very complex polylines then I suggest writing them as KML files (can also be done dynamically through the control) and presenting them through a KmlTileLayer. The KmlTileLayer leaves it up to Google to crunch through the data and return it as images.

As with all kinds of data one should be aware of information overload. Flooding the user with data is going to overwhelm them and in the end be as uninformative as too little data.

Latest Tweets