Monday, July 4, 2011

Dynamic numbered map pins with ASP.NET


A mapping project I've recently been working on needed to have coloured, numbered pins to correspond to the numbered items shown in the list below the map, e.g.
Green 23 marker

I wanted the images to be created dynamically, as the numbers could reach into the hundreds, and I didn't fancy spending several days with Photoshop. I started with empty pin images (red, green, blue and black).
Empty marker

To improve performance, I decided to write the image to disk once I'd generated it, which I could just return in future calls. I also wanted to cache the image path, so I wouldn't have to access the disk to see if the image had been created already.
I added an ImageController class, and an Index action that took the colour as a string and the number as an integer:
public ActionResult Index(string colour, int number)This would correspond to the url http://domain.com/image/green/23.png. I put the png extension so everything would know it was a png file. So after checking that an existing image for the colour and number didn't exist, one needed to be generated. I've not used the graphics classes in .NET before, so I was super excited about it. The first step was to load the correctly coloured image:
using (var stream = new FileStream(imageRoot + colour + ".png") {
  image = Image.FromStream(stream);
}
Now to draw the number on top of the image:
var g = Graphics.FromImage(image);
g.TextRenderingHint = TextRenderingHint.AntiAlias;
var stringFormat = new StringFormat
  {
    Alignment = StringAlignment.Center,
    LineAlignment = StringAlignment.Center
  };
g.DrawString(
  number.ToString(),
  SystemFonts.DefaultFont,
  Brushes.Black,
  new RectangleF(0f, 0f, image.Width, 33f),
  stringFormat);
This is just setting up the text style, defining a rectangle in the right place, then drawing the string into the rectangle. (I still can't help thinking of Speedos.) Then it was a simple case of saving the image for next time and returning it as aFilePathResult:
image.Save(imagePath, ImageFormat.Png);
return File(imageUrl, "image/png");





Sample Code :

Imports System.Web
Imports System.Web.Services
Imports System.IO
Imports System.Drawing.Text
Imports System.Drawing
Imports SHAPI.SHSafeNull
Public Class NumeredMarker
    Implements System.Web.IHttpHandler

    Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
        Try
            Dim tmpImg As System.Drawing.Image
            Dim color As String = GetString(context.Request("color"))
            Dim bgimagepath As String = ""

            Select Case Color.ToUpper
                Case "B"
                    bgimagepath = context.Server.MapPath("~/MarkerImages/BlueMarker.png")
                Case "G"
                    bgimagepath = context.Server.MapPath("~/MarkerImages/GreenMarker.png")
            End Select

             Dim stream As New FileStream(bgimagepath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
            tmpImg = Image.FromStream(stream)

            stream.Dispose()
            stream.Close()
            Dim number As Long = GetString(context.Request("No"))
            Dim g = Graphics.FromImage(tmpImg)

            Dim stringFormat = New StringFormat() With {.Alignment = StringAlignment.Center, .LineAlignment = StringAlignment.Center}
            g.DrawString(number.ToString(), New Font("Times New Roman", 9, FontStyle.Bold, GraphicsUnit.Point), Brushes.White, New RectangleF(0.0F, 0.0F, tmpImg.Width, 23.0F), stringFormat)
            context.Response.ContentType = "image/png"
            Dim ms As New MemoryStream()
            tmpImg.Save(ms, Imaging.ImageFormat.Png)
            context.Response.OutputStream.Write(ms.ToArray(), 0, CInt(ms.Length))
        Catch ex As Exception
        End Try
    End Sub
    ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
        Get
            Return False
        End Get
    End Property
End Class

No comments: