- View Online Demo (Click on X users online in directly underneath navigation bar)
Introduction
I often looked at the maps provided by my web analytic software and thought how cool it would be if I could provide these types of maps for the users currently browsing my web site, in real time. So my quest began to add this lush novelty item to my personal web site. I found the virtual maps JavaScript API and hostip.info API, and with a little bit more work and research, I had it.
The tutorial below describes how you can implement a modal popup window on your web site which will display a Microsoft Virtual Earth map with pinpoints on the locations of everyone who is currently browsing your web site.
What you will need to get started
- Microsoft Visual Studio 2008 with AJAX Controls Toolkit 3.5/3.6.
- -or- Microsoft Visual Studio 2005 with Ajax.NET Framework and AJAX Controls Toolkit 2.0.
- Virtual Earth (Microsoft Live Maps) JavaScript API.
- hostip.info API handler.
- Singleton collection for storing session information at application level.
- Global.asax file for catching and recording session info.
Using the code
I have broken the code you will need to implement, down into five steps below:
Step 1: A UserIpInfo singleton collection.
This is used to store session information for each user at the application level; this is necessary so that each user will have access to view the information of others. Alternatively, web.caching or an application variable could be used in place of a Singleton collection, but those would cause the objects to be serialized/deserialized when accessed. Since this is a novelty item, you want to make sure the addition does not hurt the performance of your website.
VB.NET
Public Class UserIPInfoList
Inherits Generic.Dictionary(Of String, UserIpInfo)
Private Shared m_instance As UserIPInfoList
Public Shared ReadOnly Property Instance() As UserIPInfoList
Get
If m_instance Is Nothing Then
m_instance = New UserIPInfoList
End If
Return m_instance
End Get
End Property
Private Sub New()
MyBase.New()
End Sub
End Class
Public Class UserIpInfo
Private m_IPAddress As String
Public Property IPAddress() As String
Get
Return m_IPAddress
End Get
Set(ByVal value As String)
m_IPAddress = value
End Set
End Property
Private m_Location As String
Public Property Location() As String
Get
Return m_Location
End Get
Set(ByVal value As String)
m_Location = value
End Set
End Property
Private m_Coordinates As String
Public Property Coordinates() As String
Get
Return m_Coordinates
End Get
Set(ByVal value As String)
m_Coordinates = value
End Set
End Property
Public ReadOnly Property CoordinatesReversed()
Get
Dim corray As String() = m_Coordinates.Split(",")
Return corray(1) & "," & corray(0)
End Get
End Property
Public Sub New(ByVal iPAddress As String, _
ByVal location As String, _
ByVal coordinates As String)
Me.IPAddress = iPAddress
Me.Location = location
Me.Coordinates = coordinates
End Sub
Public Sub New()
End Sub
End Class
C#
public class UserIPInfoList : Generic.Dictionary<string, UserIpInfo>
{
private static UserIPInfoList m_instance;
public static UserIPInfoList Instance {
get {
if (m_instance == null) {
m_instance = new UserIPInfoList();
}
return m_instance;
}
}
private UserIPInfoList() : base()
{
}
}
public class UserIpInfo
{
private string m_IPAddress;
public string IPAddress {
get { return m_IPAddress; }
set { m_IPAddress = value; }
}
private string m_Location;
public string Location {
get { return m_Location; }
set { m_Location = value; }
}
private string m_Coordinates;
public string Coordinates {
get { return m_Coordinates; }
set { m_Coordinates = value; }
}
public object CoordinatesReversed {
get {
string[] corray = m_Coordinates.Split(",");
return corray(1) + "," + corray(0);
}
}
public UserIpInfo(string iPAddress, string location, string coordinates)
{
this.IPAddress = iPAddress;
this.Location = location;
this.Coordinates = coordinates;
}
public UserIpInfo()
{
}
}
Step 2: Gathering user information in your Global.asax
You can use the global.asax file to gather information about your user on session start, and remove it on session end. The code block below will get the user's IP address, look up the location of that address, and store it in your singleton collection with the session ID as the key. When the user session ends, it will be removed based on the unique session ID.
VB.NET
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
Application.Lock()
If Session.IsNewSession() AndAlso Not _
UserIPInfoList.Instance.ContainsKey(Session.SessionID) Then
Try
Dim hostIPLookupXML As New XmlDocument
hostIPLookupXML.Load("http://api.hostip.info/?ip=" & _
Request.UserHostAddress)
Dim nsMgr As New XmlNamespaceManager(hostIPLookupXML.NameTable)
nsMgr.AddNamespace("gml", "http://www.opengis.net/gml")
Dim Hostip As XmlNode = _
hostIPLookupXML.DocumentElement.SelectSingleNode("gml:featureMember", _
nsMgr).FirstChild
If Hostip IsNot Nothing Then
Dim locationCityState As String = Hostip.ChildNodes(0).InnerText
Dim locationCountry As String = Hostip.ChildNodes(1).InnerText
If Hostip.ChildNodes.Count > 3 AndAlso _
Hostip.ChildNodes(4) IsNot Nothing Then
Dim coordinates As String = Hostip.ChildNodes(4).InnerText
If coordinates <> String.Empty Then
UserIPInfoList.Instance.Add(Session.SessionID, _
New UserIpInfo(Request.UserHostAddress, _
locationCityState & " " & locationCountry, _
coordinates))
End If
End If
End If
Catch ex As Exception
End Try
End If
Application.UnLock()
End Sub
Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
Application.Lock()
Try
Dim ipi As New BusinessObjects.ProgrammersJournal.UserIpInfo
If UserIPInfoList.Instance.TryGetValue(Session.SessionID, ipi) Then _
UserIPInfoList.Instance.Remove(Session.SessionID)
Catch ex As Exception
End Try
Application.UnLock()
End Sub
C#
public void Session_Start(object sender, EventArgs e)
{
Application.Lock();
if (Session.IsNewSession() && _
!UserIPInfoList.Instance.ContainsKey(Session.SessionID)) {
try {
XmlDocument hostIPLookupXML = new XmlDocument();
hostIPLookupXML.Load("http://api.hostip.info/?ip=" + _
Request.UserHostAddress);
XmlNamespaceManager nsMgr = new XmlNamespaceManager(hostIPLookupXML.NameTable);
nsMgr.AddNamespace("gml", "http://www.opengis.net/gml");
XmlNode Hostip = _
hostIPLookupXML.DocumentElement.SelectSingleNode("gml:featureMember", _
nsMgr).FirstChild;
if (Hostip != null) {
string locationCityState = Hostip.ChildNodes(0).InnerText;
string locationCountry = Hostip.ChildNodes(1).InnerText;
if (Hostip.ChildNodes.Count > 3 && Hostip.ChildNodes(4) != null) {
string coordinates = Hostip.ChildNodes(4).InnerText;
if (coordinates != string.Empty) {
UserIPInfoList.Instance.Add(Session.SessionID,
new UserIpInfo(Request.UserHostAddress,
locationCityState + " " + locationCountry, coordinates));
}
}
}
}
catch (Exception ex) {
}
}
Application.UnLock();
}
public void Session_End(object sender, EventArgs e)
{
Application.Lock();
try {
BusinessObjects.ProgrammersJournal.UserIpInfo ipi =
new BusinessObjects.ProgrammersJournal.UserIpInfo();
if (UserIPInfoList.Instance.TryGetValue(Session.SessionID, ipi))
UserIPInfoList.Instance.Remove(Session.SessionID);
}
catch (Exception ex) {
}
Application.UnLock();
}
Step 3: Placing the map on your page in a modal popup
The next step is to simply place the map on your page in a modal pop up window. You will need to include a reference to the JavaScript API on your page, create a function for loading the map, create a modal pop up window, and add your JavaScript function to the body onload
event. There is plenty of information available online to customize the display of your map, so I will just keep it simple here.
<script src="http://dev.virtualearth.net/mapcontrol/v3/mapcontrol.js">
</script>
<script type="text/javascript">
var map = null;
function GetMap()
{
map = new VEMap('myMap');
map.LoadMap(new VELatLong(115,-150),1,'r',false);
map.HideDashboard();
}
</script>
<body onload="GetMap();">
<asp:LinkButton ID="LinkButton2" runat="server" ToolTip="whats this?">
<asp:Literal ID="numonline" runat="server"></asp:Literal>
users online
</asp:LinkButton>
<asp:Panel ID="Panel2" runat="server" Style="display: none" CssClass="modalPopup">
<asp:Panel ID="Panel4" runat="server"
Style="cursor: move; background-color: #a5c863; padding: 3px;
border: solid 1px Gray; color: white; margin-bottom: 3px;">
<div>
<strong>Whos Online?</strong>
</div>
</asp:Panel>
<div id="myMap" style="position: relative; width: 400px; height: 200px;">
</div>
<br />
<asp:Button ID="OkButton" runat="server" Text="done" CssClass="loginbox" />
</asp:Panel>
<cc1:ModalPopupExtender ID="ModalPopupExtender1"
runat="server" TargetControlID="LinkButton2"
PopupControlID="Panel2" BackgroundCssClass="modalBackground"
OkControlID="OkButton" DropShadow="true"
Y="35" PopupDragHandleControlID="Panel4" />
Step 4: Adding pinpoints to your map
The next step is to loop through your singleton collection and add the pinpoints to your map. We will do this by dynamically creating the JavaScript in the code-behind page. You will then need to call your new function from the body onload
to display them, this is because the map needs to be created prior to adding the pinpoints.
VB.NET
Me.numonline.Text = UserIPInfoList.Instance.Keys.Count
Dim sb As New StringBuilder()
Dim count As Integer = 1
sb.AppendLine("function ShowPins()")
sb.AppendLine("{")
For Each key As String In UserIPInfoList.Instance.Keys
sb.AppendLine("var pinID = " & count & ";")
sb.AppendLine("var pin = new VEPushpin(")
sb.AppendLine("pinID, ")
sb.AppendLine("new VELatLong(" & _
UserIPInfoList.Instance(key).CoordinatesReversed & "), ")
sb.AppendLine("null, ")
sb.AppendLine("'" & UserIPInfoList.Instance(key).IPAddress & "', ")
sb.AppendLine("'" & UserIPInfoList.Instance(key).Location.Replace("'", _
"’") & "','pinEvent', 'Century 16'")
sb.AppendLine(");")
sb.AppendLine("")
sb.AppendLine("map.AddPushpin(pin);")
count = count + 1
Next
sb.AppendLine("}")
Me.Page.ClientScript.RegisterClientScriptBlock(Me.GetType(), _
"MapScript", sb.ToString(), True)
C#
{
this.numonline.Text = UserIPInfoList.Instance.Keys.Count;
StringBuilder sb = new StringBuilder();
int count = 1;
sb.AppendLine("function ShowPins()");
sb.AppendLine("{");
foreach (string key in UserIPInfoList.Instance.Keys) {
sb.AppendLine("var pinID = " + count + ";");
sb.AppendLine("var pin = new VEPushpin(");
sb.AppendLine("pinID, ");
sb.AppendLine("new VELatLong(" +
UserIPInfoList.Instance(key).CoordinatesReversed + "), ");
sb.AppendLine("null, ");
sb.AppendLine("'" + UserIPInfoList.Instance(key).IPAddress + "', ");
sb.AppendLine("'" +
UserIPInfoList.Instance(key).Location.Replace("'",
"’") + "','pinEvent', 'Century 16'");
sb.AppendLine(");");
sb.AppendLine("");
sb.AppendLine("map.AddPushpin(pin);");
count = count + 1;
}
sb.AppendLine("}");
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(),
"MapScript", sb.ToString(), true);
}
Add this to the body
tag:
<body onload="GetMap();ShowPins();">
Step 5: Customizing your CSS
There are a few customizations you will need to make to your CSS; the first is to control the display of the modal window as well as the pins on the map. The second is to add a style
tag to your aspx page so the title windows will appear over the popup window. This needs to be added after the map has loaded, so it cannot be stored in your CSS file.
/*Modal Popup*/
.modalBackground {
background-color:Gray;
filter:alpha(opacity=50);
opacity:0.7;
}
.modalPopup {
background-color:#f9f9e5;
border-width:1px;
border-style:solid;
border-color:Gray;
padding:3px;
width:400px;
text-align:center;
}
/*Map*/
.pinEvent
{
width:10px;height:15px;
overflow:hidden;
cursor:pointer;
}
<style type="text/css">
.ero{z-index: 100002 !important;}
.ero-progressAnimation{z-index: 100002 !important;}
.VE_Message{z-index: 100002 !important;}
</style>
Points of interest
- The hostip.info service is a free service, and as a free service, it performs about as good as you would expect a free service to. It seems to place any IP address it cannot determine in Camarillo, CA. If you have access to a better geo-location service or database, I would recommend using that, though the hostip.info service is extremely fast and performs well enough for a novelty addition to your web site.
- You will notice that I am using version 3 of the Virtual Maps JavaScript API. This is the only one I found to be stable, with a map this small and had the fastest loading times. If you find that you are stable using one of the other versions, please let me know.
History
- 2/20/2008 - Added C# versions of code.
- 2/28/2008 - Escapes apostrophe's in JScript output.