Google Maps in ASP.NET using LINQ to SQL (tiered)


First, let me show the inspiration for this post. The site is located at http://www.wolfpil.de/anim.html. It is a simple, cheesy kind of animation using the Google Maps API. But, it is useful for a location piece. The page looks like this:

OriginalSite

In this blog post, I am going to cover the steps necessary to make a Google map that animates, using points from a database. It is the first step in creating a location update functionality that automatically updates on a site. I am going to use this on the Crazy Cancer Tour site to be able to update from a mobile device. This is a quick and easy “how to”, and should be easy to set up following through the example.

NOTE: Everything in the current version of this article assumes Visual Studio 2008. I will go back and check for the Express versions later.

To give you an idea where I am going with this post, here is the sample project in Solution Explorer:

ProjectInSolutionExplorer

Create the Database

First, we need a database of locations. The following script will create a tour table and a location table. The tour table is to limit the mapping, which is not included in this first demo. To create the database, open SQL Server Management Studio and create a new database. When done, run this script in the new database.

CREATE TABLE dbo.Tour(
    TourId int IDENTITY(1,1) PRIMARY KEY,
    TourName nvarchar(50) NOT NULL,
    CreatedDate datetime NOT NULL,
    CreatedUserId uniqueidentifier NOT NULL
)
GO

SET IDENTITY_INSERT dbo.Tour ON
INSERT dbo.Tour (TourId, TourName, CreatedDate, CreatedUserId)
VALUES (1, N’Daily’, CAST(0x00009C66018A60CE AS DateTime), N’5e82fe2e-c973-41b4-a1be-0e345a3607f6′)
SET IDENTITY_INSERT dbo.Tour OFF
GO

CREATE TABLE dbo.Location(
    LocationId int IDENTITY(1,1) PRIMARY KEY,
    TourId int NOT NULL,
    LocationName nvarchar(50) NOT NULL,
    Latitude float NOT NULL,
    Longitude float NOT NULL,
    CreatedDate datetime NOT NULL,
    CreatedUserId uniqueidentifier NOT NULL
)
GO

SET IDENTITY_INSERT dbo.Location ON
INSERT dbo.Location (LocationId, TourId, LocationName, Latitude, Longitude, CreatedDate, CreatedUserId)
VALUES (1, 1, N’Puttgarden, Fehmarn’, 54.503143, 11.229228, CAST(0x00009C65018ACB90 AS DateTime), N’5e82fe2e-c973-41b4-a1be-0e345a3607f6′)

INSERT dbo.Location (LocationId, TourId, LocationName, Latitude, Longitude, CreatedDate, CreatedUserId)
VALUES (3, 1, N’Rødbyhavn’, 54.653278, 11.349048, CAST(0x00009C66018B2B03 AS DateTime), N’5e82fe2e-c973-41b4-a1be-0e345a3607f6′)
SET IDENTITY_INSERT dbo.Location OFF
GO

Create Data Models

The way I do this is create a project called {ProjectName].Data.Models. I then made did the following steps to get to the database:

  1. Open Server Explorer (usually with the Toolbox). If missing, it is found by Control + Alt + L (or on View >> Server Explorer)
  2. Right click on Data Connections and choose Add Connection
    CreateDataConnection
  3. Click OK

You now have a connection to the database. Add a LINQ to SQL class (CrazyCancerTour) to the project and drag the two tables on the form. It will look like this:

CrazyCancerDBML

You now have data models to work with.

Create a Data Access Layer

We now are getting a bit more complex and I have borrowed code from an earlier blog entry called “Repository Pattern in LINQ to SQL (Disconnected)”. You can click if you want some information about how I created the Repository in question. This is a Generic Repository that can work with any LINQ table.

Before coding the Repository, I need to create a DataContextFactory. The reason for this is my final project has more than one LINQ context included. First step is to create an interface. The reason for the interface is so I can test this later. Yes, I should be test driven now, but it makes for a longer post. Assume I used TDD to create this. Once I get the Crazy Cancer Dev bits up, I will revamp this post there. You will see the interface used shortly, so this is not a total wash.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Linq;

namespace CrazyCancer.Data.Access.interfaces
{
    public interface IDataContextFactory<T>
    {
        DataContext Context {get; }
    }
}

The DataContextFactory then looks like this:

namespace CrazyCancer.Data.Access.factory
{
    public class DataContextFactory<T> : IDataContextFactory<T>
        where T : class
    {
        public DataContext Context { get; set; }

        public DataContextFactory(string connString)
        {
            Type type = typeof(T);
            Context = GetDataContextType(type,connString);
        }

        public DataContext GetDataContextType(Type type, string connectionString)
        {
            string typeString = type.ToString();

            switch (typeString)
            {
                case "CrazyCancer.Data.Models.Location":
                case "CrazyCancer.Data.Models.Tour":
                default:
                    {
                        return new CrazyCancerTourDataContext(connectionString);
                    }
            }
        }

        #region IDataContextFactory<T> Members

        public void SaveAll()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

You will notice a few “tricky” things here. I am using Generics with this class. The Get DataContextType uses the Type the DataContextFactory was initialized with to determine type of DataContext. In the future, I will change the GetDataContextType so it can use a more maintainable way of getting DataContext. This works, however, for now.

As you notice, the factory loads when it is instantiated. If this were real behavior, I would shoot this idea down, but I am merely loading the “factory” up so I can pull context over and over again without re-instantiating. I will have to think about the pattern when I have more time. To use this factory, you set it up like so:

string connString = "server=(local);database=CrazyCancerTour;UID=user;PWD=password;";
IDataContextFactory<Location> contextFactory = new DataContextFactory<Location>(connString);

You will have to change the connection string to your database, of course. And, no, my password is not password on my local machine, and yours should not be either.

Our next step is creating a generic repository. Rather than go through all of the details, as this was blogged before, I will post the final code. First, we have an interface:

namespace CrazyCancer.Data.Access.interfaces
{
    public interface IRepository<T> where T : class
    {
        List<T> All();
    }
}

This interface is actually far more complex, but we are looking at retrieval only.

namespace CrazyCancer.Data.Access.repository
{
    public class Repository<T> : IRepository<T>
      where T : class
    {
        protected IDataContextFactory<T> _dataContextFactory;

        public Repository(IDataContextFactory<T> dataContextFactory)
        {
            _dataContextFactory = dataContextFactory;
        }

        public List<T> All()
        {
            return GetTable.ToList<T>();
        }

        private Table<T> GetTable
        {
            get { return _dataContextFactory.Context.GetTable<T>(); }
        }
    }
}

This will pull all of the items in the Location table, which is what we want for the time being. What we are doing here is using a bit of basic LINQ to SQL. Using a DataContext, we choose GetTable<T>, which means get all from the table <T>. When we use this in code, we are specifying Location for the table. This looks like the following code:

string connString = "server=(local);database=CrazyCancerTour;UID=user;PWD=password;";
IDataContextFactory<Location> contextFactory = new DataContextFactory<Location>(connString);

Repository<Location> repository = new Repository<Location>(contextFactory);
IEnumerable<Location> locations = repository.All();

Notice that all of the types are generic and need to have a type. What this means, to you, is you only have to code them once. I will cover the cases where you need a less generic implementation in the future.

Google Maps API bits

The next project is CrazyCancer.Framework.GoogleMaps. All I need here is a class that uses the locations and creates a JavaScript block to emit on my UI. The output I am looking for is the following (NOTE: {key} means your own Google maps API key – get one). This is my goal.

<script src="http://maps.google.com/maps?file=api&v=2&sensor=false&key={key}&quot; type="text/javascript"></script>
<script src="js/CrazyCancer.GoogleMap.Anim.js" type="text/javascript"></script>
<script type="text/javascript">

var data = [
{ name: "Puttgarden, Fehmarn", date: "8/14/2009", lat: "54.503143", lng: "11.229228" }
, { name: "Rødbyhavn", date: "8/15/2009", lat: "54.653278", lng: "11.349048" }
];

var animRoute = true;
</script>

I have moved the rest of the script to CrazyCancer.GoogleMap.Anim.js file, which looks like this (warning long script):

var map, route;
var points = [];
var gmarkers = [];
var count = 0;
var stopClick = false;

function addIcon(icon) { // Add icon attributes

    icon.shadow = "http://www.google.com/mapfiles/shadow50.png";
    icon.iconSize = new GSize(32, 32);
    icon.shadowSize = new GSize(37, 34);
    icon.iconAnchor = new GPoint(15, 34);
    icon.infoWindowAnchor = new GPoint(19, 2);
    icon.infoShadowAnchor = new GPoint(18, 25);
}

function addClickevent(marker) { // Add a click listener to the markers

    GEvent.addListener(marker, "click", function() {
        marker.openInfoWindowHtml(marker.content);
        /* Change count to continue from the last manually clicked marker
        *  Better syntax since Javascript 1.6 – Unfortunately not implemented in IE.
        *  count = gmarkers.indexOf(marker);
        */
        count = marker.nr;
        stopClick = true;
    });
    return marker;
}

function buildMap() {

    if (GBrowserIsCompatible()) {
        map = new GMap2(document.getElementById("map"));
        map.setCenter(new GLatLng(54.503143, 11.229228), 8);
        map.addControl(new GSmallMapControl());
        map.addControl(new GMapTypeControl());

        // Light blue marker icons
        var icon = new GIcon();
        icon.image = "
http://www.google.com/intl/en_us/mapfiles/ms/icons/ltblue-dot.png";
        addIcon(icon);

        for (var i = 0; i < data.length; i++) {
            points[i] = new GLatLng(parseFloat(data[i].lat), parseFloat(data[i].lng));
            gmarkers[i] = new GMarker(points[i], icon);

            // Store data attributes as property of gmarkers
            var html = "<div class=’infowindow’>" +
                    "<strong>" + data[i].name + "</strong><p>" +
                    data[i].date + "</p></div>";
            gmarkers[i].content = html;
            gmarkers[i].nr = i;
            addClickevent(gmarkers[i]);
            map.addOverlay(gmarkers[i]);
        }
        // Draw polylines between marker points
        var poly = new GPolyline(points, "#003355", 3, .5);
        map.addOverlay(poly);

        route = setTimeout("anim()", 3600);

        if (animRoute) {
            // Open infowindow of first marker
            gmarkers[0].openInfoWindowHtml(gmarkers[0].content);
        }
        else {
            if (route) {
                clearTimeout(route);
                stopClick = true;
            }
            map.setCenter(new GLatLng(0.0, 0.0), 8);
            gmarkers[data.length – 1].openInfoWindowHtml(gmarkers[data.length – 1].content);
        }
    }
}

function haltAnim() {

    if (route) {
        clearTimeout(route);
        stopClick = true;
    }
}

function carryOn() {

    if (stopClick == true) anim();
    stopClick = false;
}

function anim() {

    count++;
    if (count < points.length) {
        // Use counter as array index
        map.panTo(points[count]);
        gmarkers[count].openInfoWindowHtml(gmarkers[count].content);
        var delay = 3400;
        if ((count + 1) != points.length)
            var dist = points[count].distanceFrom(points[count + 1]);

        // Adjust delay
        if (dist < 10000) {
            delay = 2000;
        }
        if (dist > 80000) {
            delay = 4200;
        }
        route = setTimeout("anim()", delay);
    }
    else {
        clearTimeout(route);
        count = 0;
        route = null;
    }
}

function playAgain() {

    animRoute = true;
    GUnload();
    if (route) clearTimeout(route);
    stopClick = false;
    count = 0;
    buildMap();
}

My main reason for posting the whole thing is so you can duplicate this prior to me giving the download location. 🙂

Now let’s create a class that takes locations and produces the data var. To create the Google API line, I have the following routine:

public string GetGoogleMapsString(string apiKey, int versionNumber, bool useSensor)
{
    StringBuilder builder = new StringBuilder();
    builder.Append("<script src="
http://maps.google.com/maps?file=api&v=");
    builder.Append(versionNumber);
    builder.Append("&sensor=");
    builder.Append(useSensor.ToString().ToLower());
    builder.Append("&key=");
    builder.Append(apiKey);
    builder.Append("" type="text/javascript"></script>");
    builder.Append("rn");

    return builder.ToString();
}

This will create the  line:

<script src=http://maps.google.com/maps?file=api&amp;v=2&amp;sensor=false&amp;key={key}” type="text/javascript"></script>

when you supply your apiKey, the version number (can be 3 now) and whether or not to use sensor (usually false). The next routine creates the path to the CrazyCancer.GoogleMaps.Anim.js file:

public string GetMapAnimationScriptString(string pathToJavascript)
{
    StringBuilder builder = new StringBuilder();

    builder.Append("<script src="");

    if(pathToJavascript==null)
    {
        builder.Append("js/CrazyCancer.GoogleMap.Anim.js");
    }
    else{
        builder.Append(pathToJavascript);
    }

    builder.Append("" type="text/javascript"></script>rn");

    return builder.ToString();
}

I have a default path in the class. If you use a js folder, this path could work for you. If not, you can supply one by adding the parameter. Finally, we need the bits that create the data var.

public string GetInitializeString(List<Location> locations, bool animateRoute)
{
    if (locations.Count == 0)
        return string.Empty;

    StringBuilder builder = new StringBuilder();
    builder.Append("<script type="text/javascript">rn");

    builder.Append(GetJsonDataString(locations));

    builder.Append("tvar animRoute = ");
    builder.Append(animateRoute.ToString().ToLower());
    builder.Append(";rn");

    //Build end of string
    builder.Append("</script>rn");

    return builder.ToString();
}

private static string GetJsonDataString(List<Location> locations)
{
    StringBuilder builder = new StringBuilder();
    //build up the points
    builder.Append("tvar data = [rn");

    for (int i = 0; i < locations.Count; i++)
    {
        Location location = locations[i];
        builder.Append("tt");
        if (i != 0)
            builder.Append(", ");

        builder.Append("{ name: "");
        builder.Append(location.LocationName);
        builder.Append("", date: "");
        builder.Append(location.CreatedDate.ToShortDateString());
        builder.Append("", lat: "");
        builder.Append(location.Latitude);
        builder.Append("", lng: "");
        builder.Append(location.Longitude);
        builder.Append(""  }rn");
    }

    builder.Append("t];rn");

    return builder.ToString();
}

Not a huge amount of code. Note that I have separated out the GetJsonDataString. This was largely for testing the routine in isolation.

To make things easier (one call to get script), I have created a consolidating routine:

public string GetEmitString(string apiKey, int versionNumber, bool useSensor, string pathToJavascript
    , List<Location> locations, bool animateRoute)
{
    StringBuilder builder = new StringBuilder();
    builder.Append(GetGoogleMapsString(apiKey, versionNumber, useSensor));
    builder.Append(GetMapAnimationScriptString(pathToJavascript));
    builder.Append(GetInitializeString(locations, animateRoute));
    return builder.ToString();
}

When I run this with the database at hand, I end up with:

<script src="http://maps.google.com/maps?file=api&v=2&sensor=false&key={key}&quot; type="text/javascript"></script>
<script src="js/CrazyCancer.GoogleMap.Anim.js" type="text/javascript"></script>
<script type="text/javascript">
   
var data = [
        { name: "Puttgarden, Fehmarn", date: "8/14/2009", lat: "54.503143", lng: "11.229228" }
        , { name: "Rødbyhavn", date: "8/15/2009", lat: "54.653278", lng: "11.349048" }
    ];

    var animRoute = true;
</script>

This is exactly what i wanted, so I am rolling now.

Testing in a Web Application

Now that I have the application logic done, I have to add a user interface. To do this create a new website. Change out the default.aspx tagged page for this:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"
http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="
http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
<head id="Head1" runat="server">
    <link rel="stylesheet" type="text/css" href="include.css" />
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
    <meta name="author" content="Wolfgang Pichler" />
    <meta name="URL" content="
http://www.wolfpil.de" />
    <title>Map Animation</title>
    <style type="text/css">
        body
        {
            font-family: Verdana, Sans-serif;
        }
        h3
        {
            margin-left: 8px;
        }
        #map
        {
            height: 300px;
            width: 500px;
            border: 1px solid gray;
            margin-top: 8px;
            margin-left: 8px;
            overflow: hidden;
        }
    </style>
</head>

<body onload="buildMap()" onunload="GUnload()">
    <form id="form1" runat="server">
    <h3>
        Map Animation</h3>
    <div id="map">
    </div>
    </form>
</body>
</html>

The code behind is fairly simple and code you have seen already in this post. The only change is I am taking the output of the LocationBuilder and emitting it as JavaScript.

protected void Page_Load(object sender, EventArgs e)
{
    string emitScript = GetEmitScript();
    ClientScript.RegisterStartupScript(typeof(Page), "GoogleMaps", emitScript);
}

private string GetEmitScript()
{
    //Code already covered
    string connString = "server=(local);database=CrazyCancerTour;UID=sa;PWD=pass@word1;";
    IDataContextFactory<Location> contextFactory = new DataContextFactory<Location>(connString);
    Repository<Location> repository = new Repository<Location>(contextFactory);
    List<Location> locations = repository.All();

    //variables for the location builder
    string apiKey = "{key}";
    int versionNumber = 2;
    bool useSensor = false;
    string pathToJavascript = "js/CrazyCancer.GoogleMap.Anim.js";
    bool animateRoute = true;

    LocationBuilder builder = new LocationBuilder();
    string emitString = builder.GetEmitString(apiKey, versionNumber, useSensor
                       
, pathToJavascript, locationList, animateRoute);

    return emitString;
}

Remember to replace {key} with your key in the GetEmitScript() routine. You can now run the page and it looks like this:

FinalPage

You can add back in the animation buttons, if you are so inclined. Look at the source on the original page to determine how to do this. Hope you had fun with this one.

Peace and Grace,
Greg

Twitter: @gbworld

One Response to Google Maps in ASP.NET using LINQ to SQL (tiered)

  1. yusuf says:

    can u share that project with all source code?
    thanks

Leave a comment