Build an AJAX Based Map Viewer in ASP.NET 2.0
 
Published: 07 May 2007
Abstract
AJAX has become one of the coolest features for building interactive browser-based client applications. In this article you will learn how to build an Ajax.NET based map viewer in ASP.NET 2.0.
by Xianzhong Zhu
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 31537/ 47

Introduction

Nowadays, AJAX has become one of the coolest features for building interactive browser-based client applications. The famous Google Maps show a distinct advantage over traditional map service systems in availability and response speed. In this article I will show you how to build an Ajax.NET based map viewer in ASP.NET 2.0.

First, let us take a look at the final result of the map view sample, as shown in Figure 1.

Figure 1 –The final result of the map viewer sample.

The sample in this article provides the following functions:

1. When the user moves the “eagle-eye” in upper-left small map, he can see a corresponding detailed/magnified bigger one on the right side of the web page.

2. Obtain the real-time coordinates of the mouse so as to easily locate the destination.

3. Show detailed information of destination location by clicking the corresponding position in the map.

Hint: This article assumes that you understand JavaScript and basic AJAX programming. And to debug the sample provided here, you should have installed Visual Studio 2005 and SQL Server 2005 (or SQL Server 2005 Express Edition) on your machine. Also, you will need to download the open source project-AjaxPro.NET.

In this map view sample, there are three key techniques to be solved: 1. how to achieve the eagle-eye effect, 2. how to update the partial image dynamically and 3. how to popup the corresponding tips when clicking the destination in the map.

Achieving eagle-eye effect

In this demo we use a .jpg image (image/big.jpg) and a small picture of the original image with a scale of 1:5 (image/small.jpg) is shown in the eagle-eye area. Careful readers may have noticed from Figure 1 that most of the thumbnail image is translucen, and only in the middle box can the real image be seen. You can drag the box area so as to change the visible area of the thumbnail image. Listing 1 shows the code for the map viewing page.

Listing 1 –Maps.aspx

-<%@ Page language="c#" Codebehind="Maps.aspx.cs" AutoEventWireup="false" Inherits="MapViewer.Maps" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
      <HEAD>
            <title>Maps</title>
            <meta name="GENERATOR" Content="Microsoft Visual Studio .NET 7.1">
            <meta name="CODE_LANGUAGE" Content="C#">
            <meta name="vs_defaultClientScript" content="JavaScript">
            <meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
            <script language="javascript" src="javascript/dom-drag.js"></script>
            <script language="javascript" src="javascript/JSBalloon.js"></script>
            <script language="javascript">
            var oThang, oHandle;
 
            window.onload = function()
            {
                  if (document.all || document.getElementById)
                  {
                        oThang = document.all ? document.all["thang"] : document.getElementById("thang")
                        oHandle = document.all ? document.all["handle"] : document.getElementById("handle")
 
                        Drag.init(oHandle, oThang, -250, -105, -250, -158);
                        
                        oThang.onDragEnd = function(x, y)
                        {
                              refreshMap(x + 250, y + 250);
                        };
                  }
…//omitted
            </script>
      </HEAD>
      <body>
            <form id="Form1" method="post" runat="server">
                  <div id="banner"></div>
                  <h1>Ajax Map Viewer</h1>
                  <div style="LEFT:20px; OVERFLOW:hidden; WIDTH:245px; TOP:20px; HEIGHT:192px">
                        <img src="image/small.jpg">
                        <div id="thang" style="LEFT:-250px; POSITION:absolute; TOP:-250px">
                              <img
 style="FILTER:alpha(opacity=50); LEFT:60px; POSITION:absolute;
 TOP:8px" src="image/edge.gif">
                              <img
 id="handle" style="LEFT:250px; WIDTH:100px; POSITION:absolute;
 TOP:250px; HEIGHT:100px"
                                    src="image/hole.gif">
                        </div>
                  </div>
                  <div id="map" style="WIDTH: 500px; HEIGHT: 500px">
                  </div>
            </form>
      </body>
</HTML>

Here, please pay attention to the div object with id "thang" whose left corner is at point (-250px,-250px), inside which there are two pictures named edge.gif and hole.gif, respectively. The left corners of the two pictures, edge.gif and hole.gif, lie in point (0,0) and (250px,250px), respectively (relative to thang). Here, we especially design the two pictures with the size of edge.gif being 600*600 whose center is an empty transparent hole with size of 100*100. Here, image hole.gif is a picture of one-pixel and is transparent with its position just beneath the middle part of image edge.gif in the corresponding HTML code. Figure 2 illustrates the relative position of each object in the web page.

Figure 2 –The relative position of each object inside the maps.aspx page.

As seen from the figure above, we simulate the effect of a piece of translucent glass by combinating edge.gif with hole.gif—the whole area of the thumbnail image is completely visible while the other areas covered by edge.gif are translucent. In this way, we succeeded in imitating the eagle-eye effect by dragging the two images (i.e. edge.gif and hole.gif) with clearly visible area changing correspondingly.

Update the show part dynamically

Direct output

In correspondence with the eagle-eye result, we expect to dynamically update the related part of the map on the right hand of the web page-magnifying the corresponding part of the real image in the map view area. We want to put part of the factual map (big.jpg) into the viewing area. To output part of the map, a new ASP.NET Web form named pic.aspx should be added, which will output the corresponding image through the parameters-position and size. The crucial code snippet of code-behind for page pic.aspx is shown in listing 2.

Listing 2 –partial code for Pic.aspx.cs

//……'using' clauses omitted here
namespace MapViewer
{
      public class Pic : System.Web.UI.Page
      {
            private void Page_Load(object sender, System.EventArgs e)
            {
                  //Put the user-code here for initialization
 
                  //Give the path, output postion and size of the image
                  string strLocalPath = Server.MapPath("image/big.jpg");
                  int iLeft = 0, iTop = 0, iWidth = 100, iHeight = 100;
                  if (Request.QueryString["l"] != null)
                  {
                        iLeft = Convert.ToInt32(Request.QueryString["l"]);
                  }
                  if (Request.QueryString["t"] != null)
                  {
                        iTop = Convert.ToInt32(Request.QueryString["t"]);
                  }
                  if (Request.QueryString["w"] != null)
                  {
                        iWidth = Convert.ToInt32(Request.QueryString["w"]);
                  }
                  if (Request.QueryString["h"] != null)
                  {
                        iHeight = Convert.ToInt32(Request.QueryString["h"]);
                  }
                  //load the image
                  System.Drawing.Image image = new System.Drawing.Bitmap(strLocalPath);
 
                  //target region
                  Rectangle destRect = new Rectangle(0, 0, iWidth, iHeight);
                  //the initial image region
                  Rectangle srcRect = new Rectangle(iLeft, iTop, iWidth, iHeight);
                  //create a new Graphics object
                  Bitmap newImage = new Bitmap(iWidth, iHeight);
                  Graphics g = Graphics.FromImage(newImage);
                  g.SmoothingMode = SmoothingMode.HighSpeed;
                  //image output quality
                  g.CompositingQuality = CompositingQuality.HighSpeed;
                  //output to newImage object
                  g.DrawImage(image, destRect, srcRect, GraphicsUnit.Pixel);
                  //release graphics object
                  g.Dispose();
                  // release corresponding image resources
                  image.Dispose();
                  //output our image via binary stream
                  using (MemoryStream ms = new MemoryStream())
                  {
                        //image format is specified as Jpeg
                        newImage.Save(ms, ImageFormat.Jpeg);
                        //clear all output in the buffer stream
                        Response.ClearContent();
                        //set HTTP MIME type of the output stream to"image/Png"
                        Response.ContentType = "image/jpeg";
                        //output binary stream of the image
                        Response.BinaryWrite(ms.ToArray());
                  }
                  //release corresponding image resources
                  newImage.Dispose();
                  //Output
                  Response.Flush();
                  Response.End();
            }
//……

Here, among the parameters passed to pic.aspx, (l,t) represents the upper left of the image inside the whole big map, while (w,h) refers to the width and height of the image output.

With the dragging mouse coordinates, we can easily figure out the partial image in the viewing area. Thus, we add the following code (see Listing 3) into the client side of maps.aspx.

Listing 3

window.onload = function()
{
      if (document.all || document.getElementById)
      {
            oThang = document.all ?
 document.all["thang"] : document.getElementById("thang")
                        oHandle = document.all ?
 document.all["handle"] : document.getElementById("handle")
            Drag.init(oHandle, oThang, -250, -105, -250, -158);
            oThang.onDragEnd = function(x, y)
            {
                  refreshMap(x + 250, y + 250);
            };
      }
      // Update map show 
      function refreshMap(x, y)
      {
            oldX = x;
            oldY = y;
            //asynchronously invoke AJAX method
            MapViewer.Maps.GetMapInfo(x, y, GetMapInfo_callback);
      }
}

Well, so far, whenever we move the eagle-eye the partial area behind the web page it will be immediately updated, as well as show that area and the corresponding eagle-eye are consistent.

Rasterizing the output

The above outputting method is easy to achieve, while with even a short distance moving you have to update the whole content of the map. Then here arises a question-how to decrease the outputting quantity of the image? The approach introduced here is to divide the whole map into small square blocks and render by blocks. This way we can make full use of the buffering capacity in the server side and client site so as to reduce the quantity of repeatedly downloaded pictures with small quantities of map moving. So far, experienced readers may have realized the Buzz Word-AJAX. Quite right, but we will discuss details it later on.

First concentrate on dissecting the image, as is illustrated in Figure 3 below.

Figure 3 –The corresponding position of the viewing area in the whole map.

Seen from Figure 3, there are total four cases (illustrated by A, B, C and D respectively) to be noticed, as listed below:

Case A: The upper left corner of the show area is just the vertex of the map grid, which is the simplest case and we just need output the total block of picture.

Case B: The top edge of the show area just aligns with the horizontal line of the map grid, under which case other parts of the image are integer number of blocks except for the left and right areas.

Case C: The left edge of the show area just aligns with the vertical line of the map grid, under which case other parts of the image are integer number of blocks except for the top and bottom areas.

Case D: This is the most complex case, under which case the left edge of the show area aligns with neither the vertical nor the horizontal line of the map grid. So we need to deal with the four edge areas while internal parts of the image are integer number of blocks.

To adopt such a policy to output image by blocks is because we can optimize the performance of the system thanks to the buffering mechanism supplied by ASP.NET 2.0. According to the idea suggested above, we can put our project into action from two aspects- the server side and the client side.

Server side:

In this sample we use a famous open source framework – AjaxPro.NET. Above all, we should add reference to AjaxPro.dll to the project (for related details, see the accompanying downloaded guide titled "A Quick Guide How to Start.doc"). With this done, let us start to modify the crucial configure file for ASP.NET 2.0-Web.config, as shown below.

Listing 4

//……(omitted)
      <system.web>
            <httpHandlers>
                  <add verb="POST,GET" path="ajaxpro/*.ashx" type="AjaxPro.AjaxHandlerFactory, AjaxPro" />
            </httpHandlers>
            ……(omitted)
      </system.web>;

Moreover, we should do something to register the current page class.

Listing 5

private void Page_Load(object sender, System.EventArgs e)
{
      //Put the user-code here for initialization
      Utility.RegisterTypeForAjax(typeof(Maps));
}

To describe properties such as the position and URL of the image, we provide an ImageInfo class.

Listing 6

//ImageInfo class—responsible for small pieces of pictures
public class ImageInfo
{
      ///URL of the image
      private string _url;
      public string URL
      {
            get
            {
                  return _url;
            }
            set
            {
                  _url = value;
            }
      }
      ///x coordinate of the picture’ upper left corner 
      private int _left;
      public int LEFT
      {
            get
            {
                  return _left;
            }
            set
            {
                        _left = value;
                  }
            }
            ///y coordinate of the picture’ upper-left corner
            private int _top;
            public int TOP
            {
                  get
                  {
                        return _top;
                  }
            set
            {
                  _top = value;
            }
      }
      //ID identity to mark the small image
      private string _id;
      public string ID
      {
            get
            {
                  return _id;
            }
            set
            {
                  _id = value;
            }
      }
}

Next, on the server side we begin to create AJAX method named GetMapInfo (int x, int y) with parameters x and y representing the x and y coordinates of the eagle-eye in the thumbnail image, respectively. Here, with parameters x and y, we can easily figure out the corresponding show region inside the whole map and decide which case (i.e. Case A, B ,C and D) it belongs to and return the relevant image information. Here is the crucial code snippet for GetMapInfo.

Listing 7

///Returns URL and pos info of the small image to client side
/// </summary>
/// <param name="x">x coordinate of the eagle-eye's upper left corner in the thumbnail image</param>
/// <param name="y"> y coordinate of the eagle-eye's upper left corner in the thumbnail image </param>
/// <returns>information of the small image </returns>
[AjaxMethod()]
public ImageInfo[] GetMapInfo(int x, int y)
{
      //size of the raster grid
      int iStep = 100;
 
      //returned map info
      ImageInfo[] map = null;
 
      int iReminderX = (x * 5) % iStep;
      int iReminderY = (y * 5) % iStep;
 
      //According to the remainder, we calculate under four cases
      if (iReminderX == 0)
      {
            if (iReminderY == 0) //x and y directions are both integral
            {
                  //……(omitted)
            }
      }
      return map;
}

The code of the method GetMapInfo is so long that we have to cut it down. For detailed analysis of the whole code, please see the downloaded source code.

Client side:

Here we can make full use of the AJAX technique to update the show area in real-time via asynchronous AJAX invocation. The main idea behind this lies in the absolute location of the <img> object, with which we can piece together small scraps of images into a bigger one. Since the object ImageInfo returned from the server side has already included necessary information, such as coordinates and URL of the image, we can conveniently update the show area with methods provided by DOM. On the other hand, information of the map coordinates is also provided on the client side for quickly locating so that the user can see the absolute mouse position in real-time. Here is the main code in the client side.

Listing 8- SQL clause for creating the Mapinfo table.

// Update map show
function refreshMap(x, y)
{
      oldX = x;
      oldY = y;
      //asynchronous Ajax method
      MapViewer.Maps.GetMapInfo(x, y, GetMapInfo_callback);
}
 
// Callback, repaint the map region with returned map info
function GetMapInfo_callback(response)
{
      // Imageinfo object array
      var arImageInfo = response.value;
      
      //if successfully get ImageInfo array 
      if (arImageInfo)
      {
            // <div> object for map show region
            var div = document.getElementById("map");
            //delete the old pictures
            while (div.childNodes.length > 0)
            {
                  div.removeChild(div.childNodes[0]);
            }                             
            
            //iterate through every picture info
            for (var i = 0;i < arImageInfo.length;i++)
            {
                  //pre-buffering the picture using JavaScript's image object
                  var image = new Image();
                  image.src = arImageInfo[i].URL;
            }
            for (var i = 0;i < arImageInfo.length;i++)
            {           
                  //Create a new <img> object
                  var img = document.createElement("img");
                  
                  // URL of the image
                  img.src = arImageInfo[i].URL;
                  
                  //image show pos
                  img.style.position = "absolute";
                  img.style.left = 400 + arImageInfo[i].LEFT;
                  img.style.top = 80 + arImageInfo[i].TOP;
                  //……
                  img.onmousemove = function()
                  {
                        //Calculate mouse ordinates on the whole map
                        //Note, we should allow for page scrolling
                        var x = event.x + document.body.scrollLeft - 400 + 5 * oldX;
                        var y = event.y + document.body.scrollTop - 80 + 5 * oldY;
                        
                        //show mouse pos ordinates when moving the mouse
                        ShowPosInfo(x, y);
                  }
                  // append the image into <div>object
                  div.appendChild(img);
            }
      }
}
// show ordinates info in status bar
function ShowPosInfo(x, y)
{
      window.status = "x = " + x + ", y = " + y;
}
//……(omitted)
View the detailed information

For now, we have succeeded in viewing the map itself. That is, however, not enough and we should provide the corresponding auxiliary information for users to obtain more useful information from the map. As is seen from Figure 1 above, when users click any part of the map, the system will indicate some explanation. To achieve this goal, the first matter to be solved is to supply a data source. Since the image itself can not hold explaining information, we need to use data from the database to index the map information.

In this sample, indexed data are persisted in the table MapInfo and we can create it with the following SQL clause.

Listing 9- SQL clause for creating the Mapinfo table.

Create table [MapInfo] (
      [xPos] [int] NTO NULL,
      [yPos] [int] NTO NULL,
      [description] [varchar] (255) COLLATE dfsdf NOT NULL,
      CONSTRAINT [IX_MapInfo] UNIQUE NONCLUSTERED
(
      [xPos],
      [yPos],
) on [PRIMARY]
) ON [PRIMARY]
GO

Here, fields xPos and yPos represent x and y coordinates respectively, while description represents info for point (xPos, yPos).

Server side:

To get the nearest indexing information when clicking the map, we should add another server side method named GetPosInfo(int x, int y). From middle school algebra, we know the “nearest” distance equals the linear distance between the clicking point and the corresponding point. Thus, to locate the points whose linear distances from point (300,300) are less than 30, we can use the following SQL clause.

Listing 10

SELECT TOP 1 description FROM MapInfo
WHERE (xPos-300)*((xPos-300)+ (yPos-300)*((yPos -300)<30*30
ORDER BY (xPos-300)*((xPos-300)+ (yPos-300)*( (yPos -300)

Now, look into the code for GetPosInfo(int x,int y), as listed below.

Listing 11

///Obtain detailed information of the map position 
/// <param name="x">x coordination of mouse-clicking site </param>
/// <param name="y">y coordination of mouse-clicking site </param>
/// <returns> Info for the nearest mark
 ///point, while if the distance is greater than 30 an empty string is
 ///returned</returns>
[AjaxMethod]
public string GetPosInfo(int x, int y)
{
      //the max distance from the mark point
      int iR = 30;
      //to store the returned detailed info
      string strRet = "";
 
      //Get ready for connection
      SqlConnection conn = new       SqlConnection(ConfigurationSettings.AppSettings["ConnectionString"]);
 
      // SQL command
      SqlCommand cmd = conn.CreateCommand();
      cmd.CommandText = string.Format(
            "SELECT TOP 1 description FROM MapInfo " +
            "WHERE ((xPos-{0})*(xPos-{0}) + (yPos-{1})*(yPos-{1})) < {2} " +
                        "ORDER BY ((xPos-{0})*(xPos-{0}) + (yPos-{1})*(yPos-{1}))",
                        x, y, iR * iR
                        );
      try
      {
            //start connecting…
            conn.Open();
            //DataReader object
            SqlDataReader dr = cmd.ExecuteReader();
            //read description information
            if (dr.Read())
            {
                  strRet = dr.GetString(0);
            }
      }
      catch (SqlException)
      {
      }
      finally
      {
            //close the connection
            conn.Close();
      }
      return strRet;
}

Client side:

To view the detailed information of the map, the onmouseup event handler should be added to the <img> object on the web page in client-side code. In this response function we obtain the corresponding detailed map info for the mouse clicking position with asynchronous Ajax methods.

Listing 12

//response function for mouse clicking
img.onmouseup = function ()
{
      //calculate the mouse position in the big image
      //here we must allow for page scrolling
      var x = event.x + document.body.scrollLeft - 400 + 5 * oldX;
      var y = event.y + document.body.scrollTop - 80 + 5 * oldY;
      //record the mouse pos
      mouseX = event.x;
      mouseY = event.y;
      //Get the position info
      GetPosInfo(x, y);
}
//Call AJAX method to obtain postion info
function GetPosInfo(x, y)
{
      //show position info via a ballon-like tip box
      ball = new JSBalloon();
      ball.autoHide = true;
      ball.autoAway = false;
      ball.showCloseBox = true;
      //asychronously invoke GetPosInfo method
      MapViewer.Maps.GetPosInfo(x, y, GetPosInfo_callback);
}
//Callback for showing returned info
function GetPosInfo_callback(response)
{
      var strInfo = response.value;
      if (strInfo != undefined && strInfo != "" && strInfo != null)
      {
            //position info inside the tip box obtained by AJAX method 
            ball.Show({title:'Map Info',icon:'Info',message:strInfo,top:mouseY - 100,left:mouseX});
      }
}
Optimization

As is seen from above, we have used the buffering mechanism provided by ASP.NET 2.0 to render the images in the page-pic.aspx. These images, however, are all generated dynamically in the server side, when a good many resources are to be consumed. On the other hand, quite a few small pictures might be repeatedly rendered since we have rasterized the output. Thus, we can find a way of optimizing this by cutting the whole map into small static ones beforehand so as to simplify the image processing on the server side and accordingly improve the performance of the system.

We can easily build a small program to generate these small pieces of pictures automatically (the source code along with this article provides such a tool program named SplitPic). Obviously, with the small pictures changed into static ones, the AjaxMethod GetMapInfo on the server side needs to be modified correspondingly. I have left out the new code, for more details please see the downloaded source code.

Downloads

Wrap-Up

In this article we have set up a simple map viewer system with eagle-eye effects, where you can dynamically update the map without refreshing the whole page. Furthermore, it can give the details about some spot in the map. And finally, we create an algorithm to replace dynamically rendered grid pictures with static ones so as to improve system performance. Obviously, the project here is only limited to a testing case, while in a real scenario a map view system should allow users to drag the map directly, as well as give additional function such as zooming in/out, locating a target by inquiring, etc., but I will leave that part up to the readers.

 



User Comments

Title: ImageInfo   
Name: sliptnock
Date: 2008-10-08 10:45:34 AM
Comment:
In the listing 7 in public " ImageInfo [] " getmapinfo{} gives me a mistake " The type or the name of the space of names 'imageinfo' does not exist in the space of names 'MapViewer' (is reference absent of ensamblado?). To which it owes?






Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-04-19 11:49:56 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search