Creating Hover-Style Info Boxes on the Bing Maps AJAX v7.0 Control

When v7.0 of the Bings Maps AJAX control was released last fall, I began testing out how easy it would be to port some of my existing code that was originally developed against the v6.3 AJAX control. I was pleasantly surprised to find the newer version performed considerably faster, and had a more natural API. However, many of the features that v6.3 gives you OTB were not available in v7.0; info boxes being one of those features. Thus I began testing how easy it would be to develop my own info boxes. I got about 90% of the way there before running into some snags with the way mouse events and pushpins interacted that made building display-on-hover style info boxes problematic. You can read more about this experience on Windows Live Developer Forums – Creating Infoboxes in Bing Maps AJAX v7.

Microsoft recently released an update for the Bing Maps AJAX v7.0 control which includes the ability to create info boxes. The initial release did not offer this feature so this is a welcome improvement. It also seems to have addressed the mouse event issues with push-pins. Using information published in the MSDN documentation and the experiences gathered from other developer’s forum postings, I have put together an example of how to use the new info box features and create hover-style info boxes that allow clickable content. Unlike the examples provided in the API documentation, my solution doesn’t require that the user click the pin to display the info box, or click the close button to hide it . Instead, they will show by hovering the mouse over the pushpin, will stay visible as long as the mouse remains on the pushpin or info box, and will then automatically hide as the mouse moves off the info box or pushpin.

Sample code showing how this can be done is provided below.

Code Snippet
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;>
<html xmlns="http://www.w3.org/1999/xhtml&quot;>
<head>
    <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0&quot;></script>
    <script type="text/javascript">
        var map = null;
        var pinInfobox = null;

        // create a map object and place two test pins on it, with infobox on pin hover.
        function GetMap() {
            // Initialize the map
            var mapSettings = {
                // MapOptions
                credentials: "BING MAP CODE GOES HERE",
                // ViewOptions
                // default to roughly center of CO.
                center: new Microsoft.Maps.Location(39.1000, -105.6500),
                // this gives a combo arial and birdseye in v7
                mapTypeId: Microsoft.Maps.MapTypeId.birdseye,
                padding: 1,
                zoom: 7 // shows the whole state of CO
            };
            map = new Microsoft.Maps.Map(document.getElementById("myMap"), mapSettings);
            // Hide the info box when the map is moved.
            Microsoft.Maps.Events.addHandler(map, 'viewchange', mapViewChange);

            // Retrieve the location of the map center
            var pinLocation = map.getCenter();
            // Add a pin to the center of the map
            var pin = new Microsoft.Maps.Pushpin(pinLocation, { text: '1' });
            //Microsoft.Maps.Events.addHandler(pin, 'click', displayInfobox);
            Microsoft.Maps.Events.addHandler(pin, 'mouseover', pinMouseOver);
            Microsoft.Maps.Events.addHandler(pin, 'mouseout', pinMouseOut);
            // Add the pushpin to the map
            map.entities.push(pin);

            // create a second pin
            pinLocation = new Microsoft.Maps.Location(39.0000, -105.6000)
            pin = new Microsoft.Maps.Pushpin(pinLocation, { text: '2' });
            Microsoft.Maps.Events.addHandler(pin, 'mouseover', pinMouseOver);
            Microsoft.Maps.Events.addHandler(pin, 'mouseout', pinMouseOut);
            map.entities.push(pin);

        }

        // This function will create an infobox
        // and then display it for the pin that triggered the hover-event.
        function displayInfobox(e) {
            // make sure we clear any infoBox timer that may still be active
            stopInfoboxTimer(e);

            // build or display the infoBox
            var pin = e.target;
            if (pin != null) {

                // Create the info box for the pushpin
                var location = pin.getLocation();
                var options = {
                    id: 'infoBox1',
                    title: 'My Pushpin Title',
                    description: 'This is the plain text description.',
                    //htmlContent: '',
                    height: 100,
                    width: 150,
                    visible: true,
                    showPointer: true,
                    showCloseButton: true,
                    // offset the infobox enough to keep it from overlapping the pin.
                    offset: new Microsoft.Maps.Point(0, pin.getHeight()),  
                    zIndex: 999
                };
                // destroy the existing infobox, if any
                // In testing, I discovered not doing this results in the mouseleave
                // and mouseenter events not working after hiding and then reshowing the infobox.
                if (pinInfobox != null) {
                    map.entities.remove(pinInfobox);
                    if (Microsoft.Maps.Events.hasHandler(pinInfobox, 'mouseleave'))
                        Microsoft.Maps.Events.removeHandler(pinInfobox.mouseLeaveHandler);
                    if (Microsoft.Maps.Events.hasHandler(pinInfobox, 'mouseenter'))
                        Microsoft.Maps.Events.removeHandler(pinInfobox.mouseEnterHandler);
                    pinInfobox = null;
                }
                // create the infobox
                pinInfobox = new Microsoft.Maps.Infobox(location, options);
                // hide infobox on mouseleave
                pinInfobox.mouseLeaveHandler
                    = Microsoft.Maps.Events.addHandler(pinInfobox, 'mouseleave', pinInfoboxMouseLeave);
                // stop the infobox hide timer on mouseenter
                pinInfobox.mouseEnterHandler
                    = Microsoft.Maps.Events.addHandler(pinInfobox, 'mouseenter', pinInfoboxMouseEnter);
                // add it to the map.
                map.entities.push(pinInfobox);
            }
        }

        function hideInfobox(e) {
            if (pinInfobox != null)
                pinInfobox.setOptions({ visible: false });
        }

        // This function starts a count-down timer that will hide the infoBox when it fires.
        // This gives the user time to move the mouse over the infoBox, which disables the timer
        // before it can fire, thus allowing clickable content in the infobox.
        function startInfoboxTimer(e) {
            // start a count-down timer to hide the popup.
            // This gives the user time to mouse-over the popup to keep it open for clickable-content.
            if (pinInfobox.pinTimer != null) {
                clearTimeout(pinInfobox.pinTimer);
            }
            // give 300ms to get over the popup or it will disappear
            pinInfobox.pinTimer = setTimeout(timerTriggered, 300);
        }

        // Clear the infoBox timer, if set, to keep it from firing.
        function stopInfoboxTimer(e) {
            if (pinInfobox != null && pinInfobox.pinTimer != null) {
                clearTimeout(pinInfobox.pinTimer);
            }
        }

        function mapViewChange(e) {
            stopInfoboxTimer(e);
            hideInfobox(e);
        }
        function pinMouseOver(e) {
            displayInfobox(e);
        }
        function pinMouseOut(e) {
            // TODO: detect if the mouse is already over the infoBox
            //  This can happen when the infobox is shown overlapping the pin where the mouse is at
            //    In that case, we shouldn't start the timer.
            startInfoboxTimer(e);
        }
        function pinInfoboxMouseLeave(e) {
            hideInfobox(e);
        }
        function pinInfoboxMouseEnter(e) {
            // NOTE: This won't fire if showing infoBox ends up putting it under the current mouse pointer.
            stopInfoboxTimer(e);
        }
        function timerTriggered(e) {
            hideInfobox(e);
        }
    </script>
</head>
<body onload="GetMap();">
    <div id='myMap' style="position: relative; width: 500px; height: 500px;">
    </div>
</body>
</html>

, ,

  1. #1 by amoril on April 19, 2011 - 2:44 pm

    Great, example, thank you!

  2. #2 by Meghan on May 18, 2011 - 9:32 am

    You’ve really given me a headstart on using mouseover-infoboxes… but I am noticing that once I’ve triggered an infobox, the map will no longer zoom in or out. I can’t see anything obvious that would cause this; I’m seeing it on IE6 and on Firefox 4. I’ll keep poking around but thought I’d mention it here…

    • #3 by Eric Hoffman on May 18, 2011 - 10:14 am

      Meghan, I was able to duplcate the issue you described, allthough I swear that issue didn’t exist when I wrote this post. In any event, I’ve found the quick fix for it.

      Change the hideInfoBox function to check and see if pinInfobox is null first.

      function hideInfobox(e) {
          if(pinInfobox != null)
              pinInfobox.setOptions({ visible: false });
      }
      

      • #4 by Meghan on May 18, 2011 - 10:18 am

        Wow, that was an amazingly fast response – and you’re right, it fixed the issue. Thanks so much! I hadn’t found it myself, yet.

  3. #5 by Andrew on June 29, 2011 - 2:18 pm

    I believe one call has to be:

    pinInfobox.pinTimer = setTimeout(function(){timerTriggered(e)}, 300);

    for it to work in IE and Chrome as the setTimeout function has trouble passing in implicit parameters for those browsers.

  4. #6 by Andrew on June 29, 2011 - 2:24 pm

    oh never mind, tried your app. and it seems to work on those browsers. Only seems to be broken that way on my app….strange…..

    • #7 by Eric Hoffman on June 29, 2011 - 4:26 pm

      Yeah, I’ve now ran the sample code in FF 3.6/4, IE 6/7/8/9, Chrome 10/11/12, and Safari 5 (win) and didn’t notice any trouble. As near as I can tell, the setTimeout function expects either a quoted expression or a function. Given that quoted expression smell, the general approach I’ve seen is to just have it call a function like I’m doing. You could certainly wrap that function in an in-line function like you showed, but I’m not sure to what benefit. In both cases a reference to a function is being passed, which it then calls when the timer fires.

    • #8 by Eric Hoffman on June 29, 2011 - 4:43 pm

      Maybe your point was that in order to pass e on to the timerTriggered function, I’d need to do it the way you wrote it? If so, then you are correct. Had I just done it like so…

      pinInfobox.pinTimer = setTimeout(timerTriggered(e), 300);

      … then timerTriggered(3) would be evaluated and the result of that would be passed to setTimeout – not what we want.

      Passing e is inconsequential in my example though as it’s not used. I suppose I could clean up that code some but it was never intended to be used as-is, just more as an exploratory exercise to see how it all works.

  5. #9 by Andrew on June 30, 2011 - 9:39 am

    Is there a way to allow the user to select text inside the Infobox? There doesn’t seem to be an easy way to override the ‘onmousedown’ event which drags the map, but I would have imagined this should be a default functionality.

  6. #10 by Jay on August 17, 2011 - 9:52 am

    Thank you for sharing the script, Definitely going to be userful in my project.

    What are the option i can set for the infobox? what if i wanted to put a picture inside the infobox and links and etc. etc.?

  7. #11 by Jay on August 17, 2011 - 10:05 am

    And also what if i want to have different infobox for different pin? how would I achieve that? Thanx

  8. #12 by Jordan Gonzalez (@last98) on November 14, 2011 - 2:13 pm

    Thanks for the example, it’s working very similar to our 6.3 implementation.

    One problem I ran into was the pinInfoboxMouseLeave() was firing simultaneously with the pinInfoboxMouseEnter() – I fixed this by simply changing:

    function pinInfoboxMouseLeave(e) {
    hideInfobox(e);
    }

    to

    function pinInfoboxMouseLeave(e) {
    startInfoboxTimer(e);
    }

  9. #13 by beowshawitz on December 15, 2011 - 2:18 pm

    Thanks for the example. How would you associate content with a pushpin? You have the infobox text and description hard coded for each pushpin added. What is the best way to have information related to a pushpin show up dynamically in the infobox?

    • #14 by Eric Hoffman on January 3, 2012 - 10:47 am

      It’s been a while since I’ve worked on the project where I’m using Bing Maps and the code used in that project has evolved quite a bit from the sample code I posted above. I never really meant the code above to serve as a production-ready example of how to do things, but more as a learning tool to help get people up and running with their own implementation.

      That said, the way I addressed dynamic title and body content was to extend the MS pin object with some properties that allowed me to set it on the pin and then later retrieve it. That’s one of those love/hate thing with javascript… don’t have a tag property on someone else’s object? Just add your own. For example…

      var MM = Microsoft.Maps;

      // Extend the Bing Pushpin Prototypes since they took this off in v7
      if (MM.Pushpin.prototype.SetPopupTitle == null) {
      MM.Pushpin.prototype.SetPopupTitle = function (_popupTitle) { this.PopupTitle = _popupTitle; }
      }
      if (MM.Pushpin.prototype.GetPopupTitle == null) {
      MM.Pushpin.prototype.GetPopupTitle = function () { return this.PopupTitle; }
      }
      if (MM.Pushpin.prototype.SetPopupBody == null) {
      MM.Pushpin.prototype.SetPopupBody = function (_popupBody) { this.PopupBody = _popupBody; }
      }
      if (MM.Pushpin.prototype.GetPopupBody == null) {
      MM.Pushpin.prototype.GetPopupBody = function () { return this.PopupBody; }
      }

      This allows me to store title and body information on each pin and then retrieve it later. My final implementation had a function which would build all the pins I needed, setting these properties (and doing some other work), and then saving a reference to the pin in an array for later retrieval. However, you can also get a reference to the pin via events as I do in the displayInfobox function. (pin = e.target).

  10. #15 by Skillsy on December 27, 2011 - 1:07 pm

    Firstly, many thanks for this well explained and layed out example which without it, I would probably have given up attempting to convert by V6 to V7.

    I do find it strange that a function so ingrained in version 6 is missing in V7 without a lot of workarounds however I am grateful that I am doing this in a leisurely process rather than in a few years time when V6 is turned off. That would be scary.

    To answer Jay’s and probably a lot of other users who want different text, I can think of three different ways of doing this. I will cover the simplest here and post the other two later on.

    zIndex
    If you look at the pushpin options at http://msdn.microsoft.com/en-us/library/gg427629.aspx you will find that there is no real place to store a unique number without altering the pushpin – I was hoping for a “tag” element. As I use v6 without zIndex working, my dynamic pushpins are ordered from the least important to the most already so I am happy to use this simple change…

    1. Add the zIndex item to lines
    var pin = new Microsoft.Maps.Pushpin(pinLocation, { text: ‘1’, zIndex:0 });
    var pin = new Microsoft.Maps.Pushpin(pinLocation, { text: ‘2’, zIndex:1 });

    2. In the function displayInfobox, replace the line
    description: ‘This is the plain text description.’,
    description: ‘The zIndex is ‘ + pin.getZIndex().toString(),

    I hope this helps you the next stage and thanks Eric for commencing the journey.

  11. #16 by Skillsy on December 27, 2011 - 1:39 pm

    Unique text in info boxes (cont…)

    The next way of displaying unique text would be to store the pushpin ID when you create the pushpin and then se this later on. I have tried pin.getId() after the map.entities.push(pin); line however this appears to always returns null. Perhaps someone else can identify what needs to be done.

    Finally. the next way of doing this is a bit more complex, hence the second post and will require a way to store the data from each pin. We already have an almost unique key, the lat and lon. Imagine you are plotting pharmacies and there are two in one building.

    You could use the getLocation() method (returning a lat + lon) within the current pin and then loop through your collection displaying all the info for the pins at or near that location. Add a rounding factor and the current zoom level, you could get a list of nearby pushpins that overlap. That would be cool

  12. #17 by Skillsy on December 27, 2011 - 3:46 pm

    Don’t forget your mobile audience!

    One of the key reasons for our moving from v6 to v7 is the need to support the smartphone market. V6 whilst it renders, doesn’t really allow users to navigate around. V7 appears to work well but sadly the mouseover or something isn’t firing in the test script above. Is it back to the drawing board or do I have to move to Google?

    • #18 by Eric Hoffman on January 3, 2012 - 10:49 am

      I’ll see if I can get some time to check it out. The actual implementation I ended up using does work in the few mobile browsers I’ve tried, although I wasn’t specifically targeting them. I am kicking off a new project where mobile will be a major player though, so I’ll need to figure it out sooner or later.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: