Bing Maps Concepts
(formerly Virtual Earth)
Mike Garza   
VEMaps@garzilla.net   

   
Mouse Location: PushPin Location:
Lat: 00.0000 Lng: 00.0000 Lat: 00.0000 Lng: 00.0000
Boundaries:
Concept: Moveable Push Pins, Part 4: Automatic Panning

In part 4 of this series, the map has been extended to support panning. In previous examples, as the push pin was moved toward the edges of the map, you would lose sight of it when it moved off of the map. You would then have to reposition the map in order to view the push pin and work with it further.

In this example, the map will now “pan” in the appropriate direction as the push pin approaches the edge of the map. The map will continue to pan as long as the push pin is being moved toward the edge of the map within a certain range of the edge of the map.

In the example to the right, a boundary has been added to the map (as a visual indicator) which indicated the area in which panning will occur. When a push pin is being moved and it reaches a given boundary, the map will begin to pan in that direction.

NOTE: This example still needs some tweaking. Aggressive dragging of the push pin may cause the map to freak out, technically speaking. Also, since the ability to control mouse cursor is somewhat limited in JavaScript, sometimes the cursor may tend to drift off of the push pin. Use at your own risk.

Walk Through:

Since we are building off of the previous example(s), most portions of the code have not changed. In some instances code was cleaned up or tweaked. The areas that have noteworthy changes will be outlined. Otherwise you can review the source of the example to take a looks at some of the tweaks.

This example extends the previous to support automatic panning. In implementing this functionality, there were two major enhancements that needed to be made. They are establishing the boundaries where panning would occur and the process of panning itself.

“Establishing Boundaries” – In order to determine when panning will occur, boundaries on the map needed to be established. Yes, the outer edges of the map could be used as boundaries, but from a visual standpoint they did not seem to work well. Establishing boundaries slightly inward from the edges of the map seem to work better visually and functionally. In order to establish these boundaries, the following code was used:
    function setPanBounds(){
        if (panBoundsShape) {map.DeleteShape(panBoundsShape)};
        view = map.GetMapView();
        topLeft = map.LatLongToPixel(view.TopLeftLatLong);
        bottomRight = map.LatLongToPixel(view.BottomRightLatLong);
        
        var NBound = topLeft.x + 30;
        var SBound = bottomRight.x - 30;
        var EBound = bottomRight.y - 30;
        var WBound = topLeft.y + 30;
        
        var topLeftBound = map.PixelToLatLong(new VEPixel(NBound, WBound));
        var bottomRightBound = map.PixelToLatLong(new VEPixel(SBound, EBound));
        
        northBound = topLeftBound.Latitude;
        southBound = bottomRightBound.Latitude;
        westBound = topLeftBound.Longitude;
        eastBound = bottomRightBound.Longitude;
        
        document.getElementById("panBoundaries").innerHTML =    'N:' + northBound.toFixed(4) + 
                                                                ' S:' + southBound.toFixed(4) + 
                                                                ' W:' + westBound.toFixed(4) + 
                                                                ' E:' + eastBound.toFixed(4);

        var points = [
            map.PixelToLatLong(new VEPixel(NBound, WBound)),
            map.PixelToLatLong(new VEPixel(NBound, EBound)),
            map.PixelToLatLong(new VEPixel(SBound, EBound)),
            map.PixelToLatLong(new VEPixel(SBound, WBound))];
            
        panBoundsShape = new VEShape(VEShapeType.Polygon, points);
        panBoundsShape.SetLineWidth(2);
        panBoundsShape.SetLineColor(new VEColor(255,0,0, 1.0));
        panBoundsShape.SetFillColor(new VEColor(0,0,0, 0));
        panBoundsShape.Primitives[0].symbol.stroke_dashstyle = "dot";
        panBoundsShape.HideIcon();
        map.AddShape(panBoundsShape);
    }
    
Now, a good portion of this code is used for the visual representation of the boundary. You might choose not to use this in a normal application. It was added to this example to illustrate where the boundaries are so that you could anticipate when the panning would occur.

The setPanBounds function is call at several points in the application. It’s called when the map initially loads, but also called when specific events occur. If you examine the code in load function, you’ll notice that three new event handlers are registered. These events are “onendpan”, “onendzoom” and “oninitmode”. All of these event execute the same function, setPanBounds. The intent here is that as the map changes, the boundaries need to be re-established as they are based on latitude and longitude and as the map changes, so will the boundaries.

As for the setPanBounds code itself, it starts by deleting the visual representation of the boundaries if it has been established with the following line of code:
    if (panBoundsShape) {map.DeleteShape(panBoundsShape)};
    
panBoundsShape is a global variable that represents the map boundaries. Initially this value is NULL, so we test for its existence before we try to delete it. Once that housekeeping item has been taken care of, information about the physical size of the map needs to be gathered so that the boundaries can be established. This is accomplished as follows:
    view = map.GetMapView();
    topLeft = map.LatLongToPixel(view.TopLeftLatLong);
    bottomRight = map.LatLongToPixel(view.BottomRightLatLong);
    
The variable “view” is set to an instance of a VELatLongRectangle object that is returned from the GetMapView method on the VEMap object (map). This VELatLongRectangle object, view, contains two properties that describe the current boundaries of the map. These two properties are TopLeftLatLong and BottomRightLatLong. These properties contain instances of a VELatLong object that represent the top left corner and bottom right corner of the map, respectively.

Once the map boundaries are captured, the VELatLong objects are converted to VEPixel objects named topLeft and bottomRight. Initially the pan boundaries were established using VELatLong objects, but that turned out to be problematic. The amount of space a degree of latitude or longitude uses on the screen varies depending on the zoom factor. Since we wanted to establish a fixed boundary just inside the map edges, calculating that location based on pixels was more controllable. That calculation is performed as follows:
    var NBound = topLeft.x + 30;
    var SBound = bottomRight.x - 30;
    var EBound = bottomRight.y - 30;
    var WBound = topLeft.y + 30;
            
    var topLeftBound = map.PixelToLatLong(new VEPixel(NBound, WBound));
    var bottomRightBound = map.PixelToLatLong(new VEPixel(SBound, EBound));
            
    northBound = topLeftBound.Latitude;
    southBound = bottomRightBound.Latitude;
    westBound = topLeftBound.Longitude;
    eastBound = bottomRightBound.Longitude;
    
Now that we are working with pixels, the values can be manipulated a little easier. First new pixels locations are established. This is accomplished by subtracting 30 pixels from the x & y properties of the VEPixel object topLeft and bottomRight. These new values are assigned to the variables NBound, SBound, EBound and WBound. Next, two new VELatLong objects are created to represent the latitude and longitude of the corners of the panning boundaries. This is done by creating a new VEPixel object with the appropriate adjusted pixel values and passing the new VEPixel object to the PixelToLatLong method in the VEMap object (map). This method returns an instance of a VELatLong object which is assigned to the variables topLeftBound and bottomRightBound. This effectively establishes a panning boundary 30 pixels inward from the edge of the map.

Once the corners of the panning boundaries have been established, the North/South latitude boundary and the East/West boundary are captured in the variables northBound, southBound, westBound and eastBound. These variables represent the boundary lines that, if crossed, will trigger the panning process.

The remainder of this code is for creating the visual representation of the boundaries. The previous variables are used to create a new Polygon VEShape and it is added to the map. It is pretty straight forward and should be self explanatory.

“Pan handling” – Now the panning boundaries have been established, the code for handling the panning needs to be implemented. The majority of this code will be added to the existing mouseMoveHandler, but some additional logic needs to be added to the mouseDownHandler function.

In this example, the intent is to allow the movement of push pins, which are instances of VEShape classes. We’ve just added a polygon to the map, which is also an instance of a VEShape class. Because both the push pins and polygons are VEShape objects, we need to make sure that shape being right clicked for dragging is the correct type. The mouseDownHandler function was extended to take shape type into consideration. The implementation is as follows:

    //onmousedown handler
    function mouseDownHandler(e) {
        if (e.rightMouseButton && e.elementID) {
            currentShape = map.GetShapeByID(e.elementID);
            if (currentShape.GetType() == VEShapeType.Pushpin){
                moving = true;
                map.vemapcontrol.EnableGeoCommunity(true);
                document.getElementById("MapDiv").style.cursor = 'Move';
            } else {
                currentShape = null;
            }
    
As previous, the same conditions are tested (right mouse button & valid element ID) and if valid, the variable currentShape is set to the instance of the shape that is associated with the event that was raised. Since the only shape that we want to move should be a push pin, we need to evaluate the currentShape type. This is done as follows:
    if (currentShape.GetType() == VEShapeType.Pushpin){
    
The GetType() method in currentShape (VEShape object) returns a VEShapeType enumerations. VEShapeType values are VEShapeType.Pushpin, VEShapeType.Polyline, or VEShapeType.Polygon. The currentShape type is evaluated to make sure it is of the type VEShapeType.Pushpin. If it is then we know we have the proper shape to move and the remainder.

The remainder of the enhancements are contained in the mouseMoveHandler function. The code for that function is as follows:
    //onmousemove handler
    function mouseMoveHandler(e) {
        clearTimeout(resetArrows);
        var loc = map.PixelToLatLong(new VEPixel(e.mapX, e.mapY));
        var latVariance = 0;
        var longVariance = 0;
        var panAmount = 10;
        var movingDirection = '';
        
        document.getElementById("MouseLat").innerHTML = loc.Latitude.toFixed(4);
        document.getElementById("MouseLng").innerHTML = loc.Longitude.toFixed(4);
        if (moving) {
            document.getElementById("MarkerLat").innerHTML = loc.Latitude.toFixed(4);
            document.getElementById("MarkerLng").innerHTML = loc.Longitude.toFixed(4);
            map.HideInfoBox(currentShape);
            currentShape.SetPoints(loc);
            
            //determine direction
            if (previousLoc){                    
                latVariance = loc.Latitude - previousLoc.Latitude
                longVariance = loc.Longitude - previousLoc.Longitude
                
                if (latVariance > 0){
                    document.getElementById("NIndicator").style.color = 'red';
                    document.getElementById("SIndicator").style.color = '';
                    movingDirection = 'north';
                    
                } else if (latVariance < 0){
                    document.getElementById("SIndicator").style.color = 'red';
                    document.getElementById("NIndicator").style.color = '';
                    movingDirection = 'south';
                }
                
                if (longVariance > 0){
                    document.getElementById("EIndicator").style.color = 'red';
                    document.getElementById("WIndicator").style.color = '';
                    movingDirection = 'east';
                } else if (longVariance < 0){
                    document.getElementById("WIndicator").style.color = 'red';
                    document.getElementById("EIndicator").style.color = '';
                    movingDirection = 'west';
                }
                
                resetArrows = setTimeout("resetCompass()", 50);
            }
            
            //pan map
            if ((loc.Latitude < southBound) && movingDirection == 'south'){map.Pan(0, panAmount);} 
            if ((loc.Latitude > northBound) && movingDirection == 'north'){map.Pan(0, panAmount * -1);}
            if ((loc.Longitude< westBound) && movingDirection == 'west'){map.Pan(panAmount * -1, 0);}
            if ((loc.Longitude > eastBound) && movingDirection == 'east'){map.Pan(panAmount, 0);}
            previousLoc = loc;
        }
    }
    
Only a couple of changes were needed to support the automatic panning functionality. First two new variable were added.
    var panAmount = 10;
    var movingDirection = ''
    
panAmount a value that determines how much the map should be panned when panning occurs. movingDirection is a variable that indicates the current direction the push pin is being moved. How which direction the push pin being moved was covered in part three of this series.

Next the logic was added to control the panning itself. The code for this is as follows:
    //pan map
    if ((loc.Latitude < southBound) && movingDirection == 'south'){map.Pan(0, panAmount);} 
    if ((loc.Latitude > northBound) && movingDirection == 'north'){map.Pan(0, panAmount * -1);}
    if ((loc.Longitude< westBound) && movingDirection == 'west'){map.Pan(panAmount * -1, 0);}
    if ((loc.Longitude > eastBound) && movingDirection == 'east'){map.Pan(panAmount, 0);}
    
In the setPanBounds function, we defined the boundaries where panning should occur. This logic uses those values and a couple of other variables to control panning. Focusing on one direction, south, here is how this works.

The current location of the push pin is compared to the south boundary. Since the south boundary is a boundary defined by latitude, the latitude of the current location of the push pin is evaluated. If the latitude of the current location of the push pin is less than the latitude of the south boundary, then the south boundary has been crossed and panning should occur.

Additionally, before the map is panned, the direction that the push pin is being moved is validated. In this case we want to make sure that the push pin also being moved south, before the map is panned south. It could be the case that the push pin has crossed the south boundary but it is being moved back north. Testing for the direction that the push pin is being moved along with its location ensures that panning only occurs when it is needed. The concept is the same for the other directions, north, east and west.

Panning is controlled by using the Pan method on the VEMap object, map. This method accepts two parameters, x and y, which are the number of pixels the map should be panned along the respective axis. It should be noted that since we are working with pixels, the axes are relative to the top left corner of the map which is considered 0,0 on the x,y axis. Keeping that in mind for panning, the x axis would need to be increased to moved east and decreased to move west. Additionally, the y axis would need to be increased to move south and decreased to move north. In looking at the code for panning, you will see that when panning north and west, the panAmount is multiplied by -1 to convert the panAmount value to a negative number so that the location will be decreased.

Wrap-up:

The previous example shows how automatic panning can be added to the map. This would aide in the process of moving the push pin to a location that is outside the currently displayed map area.

On a personal note, while the implantation of this functionality seems somewhat simple, the journey to get here was quite involved. I went through several different approaches before I came to this one. While there may be other solutions, of the ones that I worked with, this seems to work the best.

I am very much interested in learning about other approaches to this concept. If you have one you would like to share, please drop me an email.

Any feedback, comments or questions are welcome.

Return to Home Page

©2007-2017, Mike Garza, All Rights Reserved