Google Map Control

A lot of projects need to display maps. There are ASP.NET controls available that wrap Google Maps API and expose all or most of the functionality in a C# developer friendly way. The drawback is that they become heavy weight and are another library one has to become familiar with. Our goal was to create a thin wrapper around GoogleMaps API and try to cover most frequent usage scenarios while letting developers to use maps API directly in more complex cases.

Empty map & viewports

GoogleMap control renders a single div that becomes a container for a map. Width and height specified as properties of the control are rendered as inline styles. In addition, by default, the control renders a fall-back image sized according to the width and height, that will be displayed when JavaScript is disabled. If you want to size the map yourself, you can omit Width and Height properties but also have to disable the fall-back image by setting ShowFallbackImage to false (we need to know the dimensions of the map to request appropriate fall-back image from Google).

<!-- Sizes the map using inline styles and renders fall-back image -->
<ow:GoogleMap runat="server" Width="400" Height="400" />

<!-- Will not size the map (it will not be visible if not sized with another technique). Will not render fall-back image -->
<ow:GoogleMap runat="server" ShowFallbackImage="false" />

The area displayed in the map can be controlled using a Viewport property. The control supports two viewport types: CenterAndZoom and Bounds.
CenterAndZoom viewport specifies the coordinates of the map center and zoom level to be used.
Bounds based viewport specifies TopLeft and BottomRight coordinates of the area to be shown in the map.

<ow:GoogleMap runat="server" Width="400" Height="400">
    <Viewport Center="10, 10" Zoom="5" />
</ow:GoogleMap>

<ow:GoogleMap runat="server" Width="400" Height="400">
    <Viewport Bounds="(10, 10); (20, 20)" />
</ow:GoogleMap>

Adding markers

In most cases showing just a map is not what we want. We want markers! We decided to only allow adding markers trough data binding. It seems that if you need to manually construct the markers you might be doing something exotic or complex enough for us to put you outside of the target audience.

So for this example let’s assume we have a list of objects representing our points of interest – Hospitals.

public IEnumerable<Hospital> Hospitals = new List<Hospital>() 
    { 
        new Hospital { Longitude = 0, Latitude = 0, Name = "Hospital 1", Description = "...", Photo = "..." },
        ...
    };

To display them as markers in the map we can bid the list as a datasource of our control and set LongitudeMember, LatitudeMember, and optionally TooltipMember to point to appropriate properties of our items. All of the properties accept property paths, for example LongitudeMember="Location.Longitude".

<ow:GoogleMap runat="server" 
    Width="400" Height="400"
    DataSource="<%# this.Hospitals %>" 
    LongitudeMember="Longitude"
    LatitudeMember="Latitude"
    TooltipMember="Name" /> 

If viewport is not specified, the control will automatically adjust the visible map area to fit all markers. If a data source is empty, the control will not be able to adjust the viewport. To provide a fallback, specify viewpoer and set AutoViewport property to true. This way if data source is not empty, auto viewport will be used. Otherwise the viewport specified will be used.

Info windows

Info windows are the popup callouts that show up after you click on the marker. The content of info windows can be specified using a common ASP.NET technique – a template.

<ow:GoogleMap runat="server" DataSource="<%# this.Hospitals %>" ... > 
    <InfoWindowTemplate>
        <h1> <%# Html.Encode(Eval("Name")) %></h1>
        <img src='<%# Eval("Photo") %>' />
    </InfoWindowTemplate>
</ow:GoogleMap>    

Strongly typed data sources

In addition to the above, GoogleMap control has also a generic version GoogleMap<T> where T is the type of an item in the DataSource. To create a typed GoogleMap, one has to inherit from GoogleMap<T> and provide functions used to resolve latitude, longitude and optionally a tool-tip:

public class HospitalsMap : GoogleMap<Hospital>
{
    public HospitalsMap()
        : base(h => h.Location.Latitude, h => h.Location.Longitude, h => h.Name)
    {
    }

    [PersistenceMode(PersistenceMode.InnerProperty)]
    [TemplateContainer(typeof (GoogleMapInfoWindowContainer<Hospital>))]
    public new ITemplate InfoWindowTemplate
    {
        get { return base.InfoWindowTemplate; }
        set { base.InfoWindowTemplate = value; }
    }
}

Later the control can be used as follows:

<a:HospitalsMap runat="server" DataSource="<%# this.Hospitals %>" ... > 
    <InfoWindowTemplate>
         <!-- typed access to items -->
        <%# Html.Encode(Container.Item.Description) %>
    </InfoWindowTemplate>
</a:HospitalsMap>

Behind the scenes

One of the most important requirements for this control is to not introduce constraints that would prevent customizations and extensions of the basic scenarios above. We hope we can achieve this using the following markup that we render (formated for clarity).

<script type="text/javascript">
var mapControl = new openWaves.GoogleMapControl(
    {"mapContainerId":"mapControl",
     "dataItems":[
            {"location":{"Latitude":0,"Longitude":0},"toolTip":"Hospital 1","infoWindowContent":"Lorem ipsum dolor"},
            {"location":{"Latitude":-20,"Longitude":0},"toolTip":"Hospital 2","infoWindowContent":"Lorem ipsum dolor"},
            ... ],
     "viewport":{"Center":null,"Zoom":-1,"Bounds":{"TopLeft":{"Latitude":22,"Longitude":-20},"BottomRight":{"Latitude":-21,"Longitude":20}}},
     "markerIconUrl":"/markerIcon.png"});
</script>
 
...

<div id="mapControl" />

In addition to the above, the control renders by default a one line startup script that initializes the map:

<script type="text/javascript">
//<![CDATA[
mapControl.initialize();//]]>
</script>

Auto initialization can be turned off by setting InitializeGoogleMap property of the control to false. This allows manual initialization of the map using just the serialized markers definition if we don’t think out of the box initialization works for us.

Even if we decide to automatically initialize the map, there is plenty of ways to customize the results. JavaScript variable that is created by the control can be used to set various map options and intercept some of the events.

Setting MapOptions

Because the actual initialization of the map is postponed, any changes we make to mapOptions will be used when initializing the map. This way, anything that can be done with the API can still be done when using the control.

<a:GoogleMap runat="server" ID="GoogleMap" ... > 
    ...
</a:GoogleMap>
 
<script type="text/javascript">
    (function(mapControl) {
        mapControl.mapOptions.zoomControl = false;    // Disable zoom control
        mapControl.mapOptions.scrollwheel = false;    // Disable scrollwheel zooming
    })(<%=GoogleMap.ClientID %>);
</script>

Intercepting marker clicks

By default the control will render ‘click’ handler that opens associated info windows. This behavior can be changed using the following snipped.

<script type="text/javascript">
    (function(mapControl) {
        mapControl.onMarkerClicked = function(map, marker) {
            //
            // marker.dataItem gives access to marker data from the control DataSource
            // marker.InfoWindow gives access to an info window (if one was created)
            //
            alert(marker.dataItem.toolTip); 
        };
    })(<%=HospitalsMap1.ClientID %>);
</script>

Customizing how markers are added to the map

By default the control will add markers to the map as they are created. Sometimes this is not exactly what we want. For example to use Fluster2 library that provides clustering of markers, instead of adding the markers to the map, we need to add them to a fluster instance. This is how this can be done.

<script type="text/javascript">
    (function(mapControl) {
        var fluster = ...;
 
        mapControl.onMarkerCreated = function(map, marker) { 
            fluster.addMarker(marker);
        };
    })(<%=GoogleMap.ClientID %>);
</script>

Adding more properties to marker data items

Very often you will need more then just the position and a tooltip to be associated with a marker. You might want to use this additional data to for example decide on the icon for the marker, or use it to filter the markers in the client script. The control allows you to add arbitrary properties to items rendered as part of the control declaration (see Behind the sceens” section). To do it you will need to handle ItemDataBound event on the control. In the handler you can add any property (of any type that can be serialized to JavaScript object).

protected void Map_ItemDataBound(object sender, GoogleMapItemDataBoundEventArgs e)
{
    //
    // This will tell us what icon to show for a hospital
    //
    e.Item["hospitalType"] = ((Hospital) e.DataItem).Type;
}

The item is later added as a property to each marker, so you can use it for example to set the marker icon:

<script type="text/javascript">
    (function(mapControl) {
        mapControl.onMarkerCreated = function(map, marker) { 
            marker.setIcon(marker.item.hospitalType + '.png');
            marker.setMap(map);
        };
    })(<%=GoogleMap.ClientID %>);
</script>

Another possible scenario is filtering the markers according to some logic:

<script type="text/javascript">
    (function(mapControl) {
        $('#showBigAndFancyOnly').click(function() {
            mapControl.filterMarkers(function (marker) {
                return marker.hospitalType === 'BigAndFancy';
            });
    })(<%=GoogleMap.ClientID %>);
</script>

Last edited Jul 31, 2012 at 7:43 AM by mgrzyb, version 5

Comments

No comments yet.