Virtual Earth Workout Tracker - Part II

by Gong Liu March 31, 2009 00:45

In Part I, I decribed what the Virtual Earth Workout Tracker was, and how it could be a useful tool for tracking one's workout and motivating him to keep going. In this part, I'll describe how to implement the Workout Tracker with Virtual Earth SDK.

Generating the Route

Normally, you want to generate a route that is closely resembling the physical thing on the ground, whether it's a highway or things like the Greate Wall of China. There are basically two ways to generate a route: trace the route manually with the aid of some mapping tool, or generate it automatically with a route engine. The following screenshot shows how you could trace a stretch of highway, say Route 66, using Google Earth.

Google Earth will save the route you are tracing in KML format, which is just XML understandable by any XML parser. As to how many points you need, depending on the length and geometry of the route, a few hundread to a couple of thousand points seem to be a good number. You need more points to trace the route more closely, but it will take longer time to get it done and longer computing time to render the route.

If your route is along drivable roads, you could probably generate the route automatically using some route engine, such as the one in Virtual Earth or Google Earth. One issue with a route engine is that it has its own mind. The route it genetares may not include all the roads you want. In this case, you will need to set up a multi-waypoint route. The waypoints provide some sort of a guideline on how the route should be generated. In Virtual Earth, once a route is generated, you can get all the points that describe the shape of the route with the VERoute.ShapePoints property. Note that this property is only available to licensed Virtual Earth developers.

If you generate the route with Google Earth, you get all the details about the route in a KML file, something like:

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.2">
<Document>
 <name>KmlFile</name>
 <Style id="roadStyle">
  <LineStyle>
   <color>7fcf0064</color>
   <width>6</width>
  </LineStyle>
 </Style>
 <Placemark>
  <name>Route</name>
  <description>
   <![CDATA[Distance: 3,034 mi Map data 2009 Maponics, Sanborn, Tele Atlas]]>
  </description>
  <styleUrl>#roadStyle</styleUrl>
  <MultiGeometry>
   <LineString>
    <coordinates>
      -118.38541,33.83817,0 -118.38331,33.83788,0 -118.38157,33.83755000000001,0 -118.38086,33.83751,0 ......
    </coordinates> 
   </LineString>
  </MultiGeometry>
 </Placemark>
</Document>
</kml>

Polyline Reduction and Douglas-Peucker Algorithm

For a long route generated with a route engine, the number of points can be huge. For example, a coast-to-coast cross country route generated with Google Earth contains some 12,500 points. To draw the route with so many points would cause significant delay when the Workout Tracker is started. To improve the performance, we need to reduce the number of points, but at the same time maintain the general shape of the route. This type of problem is called polyline reduction or polyline simplification. One of the best-known algorithms for polyline reduction is the Douglas-Peucker algorithm. The following series of diagrams from McNeel Wiki best illustrate how the algorithm works:   

 

 

 

I found a C# implementation of the algorithm here. I modified the code to accept latitude/longitude points instead of integer points. I also added to the algorithm an interface that reads a polyline (route) from a KML file and writes the reduced polyline to a javascript file in form of a javascript array: 

var route = new Array(
new VELatLong(33.83817,-118.38541),
new VELatLong(33.83748,-118.35352),
new VELatLong(33.85775,-118.35359),
......
);

The screenshot below is the interface to the polyline reduction algorithm:

The single parameter that controls which points are removed and how many points are removed is the tolerance, which is the maximum allowable distance from a point to the nearest line segment (the dashed blue lines in the above series of diagrams). The total route distance can also serve as a crude measurement about how well a reduction is. The following table shows the point count and total distance vs. the tolerance for my cross country route:

The data in the table shows that as the tolerance increases, the number of points needed to describe the shape of the route decreases. In extreme, when the tolerance is big enough, only 2 points, the starting point and the ending point, are needed. Notice that as the tolerance increases, the route distance becomes shorter and shorter as compared to the original route distance. In my case, I use the route with 614 points (tolerance = 0.005°). This represents a 95% point reduction, while the route distance is only about 32 miles shorter or 1% shorter than the original route. Even at this level of reduction, you won't notice the discrepancy between the orginal route and reduced route up to about zoom level 9 (Virtual Earth has total 19 zoom levels, with the zoom level 19 representing the closest level to the ground). If you zoom in closer, you will see the difference, though. 

You may prefer a route with less reduction and thus less discrepancy, but you need to strike a balance between accuracy and the loading speed.

Implementation

There are 4 layers of data on the map. The first layer contains the route (cyan color). The second layer contains the progress polyline (red color). The third layer contains the workout day markers (red little requares). The fourth layer contains the starting point (green pushpin) and the ending point (red pushpin).

The route is loaded on the map with this script:

function LoadRoute()
{
    var layer = new VEShapeLayer();   //layer 1
    map.AddShapeLayer(layer); 
    var shape = new VEShape(VEShapeType.Polyline, route);
    shape.HideIcon();
    shape.SetLineColor(new VEColor(0,255,255,0.5));
    shape.SetFillColor(new VEColor(0,255,255,0.5));
    shape.SetLineWidth(5);
    layer.AddShape(shape);        

Here, route is the javascript array generated by the polyline reduction program mentioned earlier.

For simplicity, we use the following 2D javascript array to hold our workout data:

var data = new Array(
//Date, Comment, Fitness Machine, Mileage, Calories, Fitness Machine, Mileage, Calories, ...
new Array("2/24/2009", "A Long Journey Begins with the First Step", "Stationary Bike", 9, 150, "Stair Master", 2.3, 280),
new Array("2/25/2009", "", "Stationary Bike", 10, 170, "Elliptical Trainer", 1.7, 168, "Treadmill", 3, 340),
new Array("2/26/2009", "", "Stationary Bike", 9, 150, "Stair Master", 2.3, 280),
new Array("2/27/2009", "", "Stationary Bike", 9, 150, "Treadmill", 3, 350),
......
);

Each row in the above array represents a workout day, and each workout day has a date field, a comment field, and one or more workout items with each workout item containing such information as fitness machine used, distance covered, and calories burned. Of course, in a real application the workout data is most likely stored in a backend database.

To facilitate the manipulation of the workout data, we further define the following two javascript objects, WorkoutDay and WorkoutItem:

function WorkoutDay(dt, comment, items) {
    this.Date = dt;
    this.Comment = comment;
    this.Items = items;
    this.Point = null;
    this.CumulatedDistance = 0;
    this.CumulatedCalories = 0;
    this.GetDayMiles = function() {
        var m = 0;
        for(var i = 0; i < items.length; i++)
            m += items[i].Miles;
        return m;
    };
    this.GetDayCalories = function() {
        var c = 0;
        for(var i = 0; i < items.length; i++)
            c += items[i].Calories;
        return c;
    };
    this.GetHTML = function() {
        var s = "<table cellspacing='0' cellpadding='2' border='0' width='200'>";
        for(var i = 0; i < items.length; i++)
        {
            s += "<tr>";
            s += "<td>Type:</td>";
            s += "<td><img src='Images/" + items[i].GetTypeImage() + "' alt='" + items[i].Type + "'></td>";
            s += "</tr>";
            s += "<tr>";
            s += "<td>Distance:</td>";
            s += "<td>" + items[i].Miles + " miles</td>";
            s += "</tr>";
            s += "<tr>";
            s += "<td>Calories:</td>";
            s += "<td>" + items[i].Calories + "</td>";
            s += "</tr>";
            s += "<tr>";
            s += "<td colspan='2'><hr></td>";
            s += "</tr>";
        }
        s += "</table>";
        s += "<table cellspacing='0' cellpadding='2' border='0' width='200'>";
        s += "<tr><td>Day total dist:</td><td>" + this.GetDayMiles() + " miles</td></tr>";
        s += "<tr><td>Day total calories:</td><td>" + this.GetDayCalories() + "</td></tr>";
        s += "<tr><td>Cumulated dist:</td><td>" + this.CumulatedDistance.toFixed(1) + " miles</td></tr>";
        s += "<tr><td>Cumulated calories:</td><td>" + this.CumulatedCalories.toFixed(1) + "</td></tr>";
        if (this.Comment != "")
        {
            s += "<tr><td colspan='2'><hr></td></tr>";
            s += "<tr><td colspan='2'>" + this.Comment + "</td></tr>";
        }
        s += "</table>";
        return s;
    };
}

function WorkoutItem(type, miles, calories) {
    this.Type = type;
    this.Miles = miles;
    this.Calories = calories;
    this.GetTypeImage = function() {
        // Replace " " with ""
        var re = / /g;
        var r = type.replace(re, "");
        return r.toLowerCase() + ".gif";
    };

The WorkoutDay.Items property is an array of WorkoutItem objects. The WorkoutDay.GetHTML method generates a HTML snip to be presented in the InfoBox of the workout day marker. It includes such info as the workout activities and statistics for the current workout day. The WorkoutDay.CumulatedDistance property represents the cumulated workout mileage from all previous workout days through the current workout day. The WorkoutDay.Point property is a VELatLong object that marks the position of the current workout day along the route.

Referring to the above diagram, P0 ... Pn are route points. Q is the WorkoutDay.Point, and its coordinates can be determined in the following way:

  • Calculate WorkoutDay.CumulatedDistance, which is denoted by cd
  • Calculate the cumulated route distance from P0 through Pi-1, which is denoted by cdr
  • Calculate the distance between Pi-1 and Pi, which is denoted by D
  • Whether Q is in between Pi-1 and Pi is determined by this condition: cdr < cd < cdr + D 
  • Calculate d = cd - cdr
  • Calculate the coordinates of Q by linear interpolation: 
      

Finally, we are ready to show the script for loading the second, third and fourth layers of data:

function LoadData()

    //populate an array of WorkoutDay objects with workout data
    workoutdays = new Array(data.length);
    for (var i = 0; i < data.length; i++)
    {
        var items = new Array();
        for (var j = 2; j < data[i].length; j += 3)
        {
            var item = new WorkoutItem(data[i][j], data[i][j+1], data[i][j+2]);
            items.push(item);
        }
        var dt = new Date(Date.parse(data[i][0]));
        var comment = data[i][1];
        var wd = new WorkoutDay(dt, comment, items);
        workoutdays[i] = wd;
    }
    //Calculate cumulated mileage, calories and workout day position
    for (var i = 0; i < workoutdays.length; i++)
    {
        if (i == 0)
        {
            workoutdays[i].CumulatedDistance = workoutdays[i].GetDayMiles();
            workoutdays[i].CumulatedCalories = workoutdays[i].GetDayCalories();
        }
        else
        {
            workoutdays[i].CumulatedDistance = workoutdays[i-1].CumulatedDistance + workoutdays[i].GetDayMiles();
            workoutdays[i].CumulatedCalories = workoutdays[i-1].CumulatedCalories + workoutdays[i].GetDayCalories();
        }
        workoutdays[i].Point = GetProgressPoint(workoutdays[i].CumulatedDistance);
    }
   
    //load progress polyline to the second layer
    var layer2 = new VEShapeLayer(); 
    map.AddShapeLayer(layer2);
    var points = new Array();
    for (var i = 0; i < lastDayIndex; i++)
        points[i] = route[i];
    points.push(workoutdays[workoutdays.length-1].Point);
    var shape = new VEShape(VEShapeType.Polyline, points);
    shape.HideIcon();
    shape.SetLineColor(new VEColor(255,0,0,0.5));
    shape.SetFillColor(new VEColor(255,0,0,0.5));
    shape.SetLineWidth(7);
    layer2.AddShape(shape);      
   
    //load workout day markers to the third layer
    var layer3 = new VEShapeLayer(); 
    map.AddShapeLayer(layer3);
    var shapes = new Array();
    for (var i = 0; i < workoutdays.length; i++)
    {
        var sh = new VEShape(VEShapeType.Pushpin, workoutdays[i].Point);
        sh.SetTitle(workoutdays[i].Date.toDateString() + " - Day: " + (i + 1));
        sh.SetCustomIcon("<div style='margin-top: 11px; margin-left: 10px'><img src='images/redsquare.gif' width='5' height='5'></div>");
        sh.SetDescription(workoutdays[i].GetHTML());
        shapes[i] = sh;
    }
    layer3.AddShape(shapes);
   
    //load starting and ending points to the fourth layer
    var layer4 = new VEShapeLayer(); 
    map.AddShapeLayer(layer4);
    var startPt = new VEShape(VEShapeType.Pushpin, route[0]);
    startPt.SetTitle(od[0][0]);
    startPt.SetCustomIcon("<div style='margin-top: -31px; margin-left: -5px'><img src='images/" + od[0][1] + "'></div>");
    startPt.SetDescription(od[0][2]);
    startPt.SetMoreInfoURL(od[0][3]);
    layer4.AddShape(startPt);
    var endPt = new VEShape(VEShapeType.Pushpin, route[route.length - 1]);
    endPt.SetTitle(od[1][0]);
    endPt.SetCustomIcon("<div style='margin-top: -32px; margin-left: 6px'><img src='images/" + od[1][1] + "'></div>");
    endPt.SetDescription(od[1][2]);
    endPt.SetMoreInfoURL(od[1][3]);
    layer4.AddShape(endPt);
}

function GetProgressPoint(cd)
{
    var pt1 = route[0];
    var pt2;
    var cdr = 0;
    for (var i = 1; i < route.length; i++)
    {
        pt2 = route[i];
        var D = GetDistance(pt1, pt2);
        if (cdr + D > cd)
        {
            var d = cd - cdr;
            var k = d/D;
            lastDayIndex = i;
            return new VELatLong(pt1.Latitude + k * (pt2.Latitude - pt1.Latitude), pt1.Longitude + k * (pt2.Longitude - pt1.Longitude));
        }
        cdr += D;
        pt1 = pt2;
    }
    lastDayIndex = route.length - 1;
    return pt2; //last route point or destination
}

function GetDistance(pt1, pt2)
{
    var EarthRadiusInMiles = 3963.23;
    var RadiansPerDegree = 0.017453292519943295;
    return EarthRadiusInMiles * RadiansPerDegree * Math.sqrt((pt2.Latitude - pt1.Latitude) * (pt2.Latitude - pt1.Latitude) +
            Math.pow((pt2.Longitude - pt1.Longitude) * Math.cos(RadiansPerDegree * 0.5 * (pt1.Latitude + pt2.Latitude)), 2));
}

The above formula is used in the function GetProgressPoint(). The distance formula in GetDistance() was discussed in this post.

The touring/slide show feature was discussed in this post.

Further Improvements

Here are some ideas for further improvements on the Workout Tracker:

  • Use a database to store the workout data
  • Provide a web interface to allow a user to enter his workout data into the database
  • Provide a mobile interface so that a user can capture his workout data in the gym and send it out to the server via his cell phone
  • Support a group of users

Virtual Earth Workout Tracker - Part I

by Gong Liu March 26, 2009 11:27

I picked up a 24 Hour Fitness membership last Thanksgiving, but I realized that I would need something to motivate myself to go to the gym as often as I could. I remembered when I was in junior high school we had a long-distance running event. The goal of the event was to cumulate the running mileage of all students in a class to reach a number that symbolized something. Of course, in that era of China, every event had to be political. We symbolized our running as the Long March led by Chairman Mao in 1934-35, which covered some 8000 miles. I remembered we had a paper map that highlighted our symbolized route. Each day our class monitor would advance a little flag pin along the route to show our collective progress. We were encouraged to compare our map with that of other classes throughout the school. Man, that little map motivated me so much that I would run 10 kilometers a day almost every day for the entire event!

So I set out to create an electronic version of the paper map I had in junior high. It will serve the purposes of tracking my workout and motivating me by showing my daily progress against my goals. The result is the Virtual Earth Workout Tracker, as shown below.

Main Features

The workout tracker has these features:

  • It clearly shows the symbolized route and my workout progress on Virtual Earth. I can zoom and pan the map any way I want.
  • At the bottom is a bar graph that shows my progress against my goals in terms of days passed (which include workout days and resting days), cumulated distance, and cumulated calories .
  • Each workout day is marked as a little red square along the route. Clicking a workout day marker will show me the workout activities and statistics for that day.
  • I can play a slide show about all my workout days with the controls at the bottom-left corner.

Goal Setting 

My primary goal is to go to the gym or do some kind of outdoor activities as often as I can so that fitness becomes part of my life style. My plan is to cross the US coast to coast in one year, symbolicly that is. The total distance is about 3000 miles. I can make it if I workout 300 days of the year at the rate of 10 miles per day. That leaves me one day of rest per week, plus some holidays. Of course running 10 miles per day is unrealistic for my condition. So I will have to "cheat" a little bit - every activity by any means counts. All the cardio machines in the gym count, treadmill, elliptical trainer, stairmaster, stationary bike, etc. All outdoor activities count, jogging, biking, hiking, swimming, etc. I'm allowed to collect about 9 miles a day simply by biking to and from my workplace, for example.

My secondary goal is to lost some weight and build strength. I intend to lost 20 pounds to reach my desired body weight, which means I need to burn at least 70,000 calories in one year. All cardio machines in the gym have calorie readings, even though they are not terribly accurate. By my experience elliptical trainers tend to over estimate calories burned, while treadmills and stairmasters tend to do the oppsite. Counting calories burned druing weight lifting is too tedious, so I'll simply ignore it. For outdoor activities, I will use the following table as reference:

   

These numbers are based on the Health Calculators found here. Calorie-intake and basic metabolic rate are also very important. But again they are too tedious to count. I'll simply multiply the original calorie goal by a factor of 2 to accomadate all the uncertainties. So my final calorie goal is 140,000 160,000 175,000 calories per year.

About the Route

The route I choose has no particular meaning other than the fact that it is cross country and about 3000 miles. It starts from Redondo Beach, CA and ends at Plymouth, MA. The route is actually a drivable route generated by Vertual Earth route engine. Of course, if you want, you can choose some famous historical route or a route that is meanful to you. Here are some ideas:

In Part II, I will disciss the technical aspect of the Virtual Earth Workout Tracker.

Tips & Tricks of Using 1&1 Shared Hosting Environment

by Gong Liu March 08, 2009 06:33

I use 1&1 (1and1.com) shared hosting for this blog site and some of my other projects because of its reasonally good set of features and ridiculously low price. You can get a business account with 250GB web space and 2500GB monthly transfer volume for only 10 bucks a month. However, a shared web hosting environment such as 1&1 has some limitations that a real web administrator may not get used to at first. As our company's sr. developer/ administrator, I have full control over some 30 web servers, application servers and database servers via remote desktop. I have full administrator rights. I can run any applications I want on these servers. But with my shared hosting account at 1&1, I can't even access the IIS admin console or the SQL Server 2005 Management Studio. On the development side, the limitations include: you can't configure all the web application features via your web.config file; you can't connect to other web servers/sites from your code without going through a proxy server.

In the following sections I'll share with you some tips and tricks I found out through numerous emails and phone calls to the sometimes not-so-helpful 1&1 technical support. Note that the tips and tricks are based on 1&1 MS Shared Hosting package (Windows Server 2003, IIS 6.0, SQL Server 2005, and .Net Framework 2.0). However, the general ideas may be applicable to similar hosting environment offered by other hosting companies.

Sending Emails from a Web Application

First of all, you CAN NOT configure the mail server in your web.config file like so:

    <system.net>
        <mailSettings>
            <smtp from="admin@mydomain.com">
                <network host="smtp.1and1.com" password="mypassword" userName="myusername" />
            </smtp>
        </mailSettings>
    </system.net>

This configureation section will simply be ignored by the shared server.      

Secondly, the mail server "smtp.1and1.com" does not need a userName/password. It doesn't mean the mail server is open to public, though. You have to be an 1&1 customer and you can only send out emails from your application running on one of the 1&1 web servers. You can not use the mail server from your application running on your development machine at your home, for example.

If you want to, you can define the mail server host and the sender email address in the appSettings section of your web.config file, such as:

  <appSettings>
    <add key="SmtpHost" value="smtp.1and1.com" />
    <add key="Sender" value="admin@mydomain.com" />
  </appSettings> 

Here is the sample code in C# for sending an email using the above configuration:

    try
    {
        MailMessage mm = new MailMessage();
        mm.From = new MailAddress(ConfigurationManager.AppSettings["Sender"]);
        mm.To.Add(yourname@somedomain.com);
        mm.Subject = "Test from 1&1 Mail Server";
        mm.IsBodyHtml = false;
        mm.Body = "This is a test email.";
        SmtpClient smtp = new SmtpClient();
        smtp.Host = ConfigurationManager.AppSettings["SmtpHost"];
        smtp.Send(mm);
    }
    catch (Exception ex)
    {
        //handle the exception here
    } 

Communicating with PayPal Server via a Proxy Server

For security reasons your web application on a shared web server can not contact an outside server directly. It has to go through a proxy server. For example, if you design a website that accepts donations via Paypal and you want PayPal to instantly notify your application the status of a transaction (Instant Payment Notification, or IPN), then your application needs to communicate with PayPal server via a proxy server, as shown in the following diagram.

   paypal flow

The flow of communications among a client and the servers involved is as follow:

  1. The client requests a donation page from the web server that is running your website/application.
  2. The web server returns such a page, which includes a web form with at least a textbox for donation amount and a submit button.
  3. The client enters the amount to donate and clicks the submit button to submit the form to the PayPal server for payment processing.
  4. There are a couple of round trips between the client and the PayPal server for such activities as logging in the client's PayPal account, verifying payment info, and acknolodging the payment.
  5. At the end of the series of activities in (4) is a page that allows the client to return to your website either automatically or by pressing a return button.
  6. The client returns from PayPal website to a thank-you page on your website. The url of the thank-you page is specified in the donation page in (2) or pre-configured in your PayPal account. At this point, the actual transaction - transferring money from the client's PayPal account to your PayPal account - may or my not have happened yet. All you care about is that whenever it happens your application should know so that it can take appropriate actions.
  7. When the transaction does happen, the PayPal server sends the transaction detail to your application on the web server by requesting an IPN page. The url of the IPN page is specified in the donation page in (2) or pre-configured in your PayPal account.
  8. Your application responses the request with the transaction detail received in (7), plus a notification validation flag (_notify-validate). This is the step where you use the proxy server to send message back to the PayPal server.
  9. The PayPal server verifies the message and sends back the validation status. If the status is VERIFIED, you may want to update a database with the transaction detail and/or email a receipt to the client. If the status indicates some kind of error, you may want to log the error for further analysis.

The C# code for the IPN page looks like this:

    protected void Page_Load(object sender, EventArgs e)
    {
        //Parse transaction detail from Paypal 
        byte[] param = Request.BinaryRead(Request.ContentLength);
        string strRequest = Encoding.ASCII.GetString(param);
        Dictionary<string, string> nvs = new Dictionary<string, string>();
        string[] items = strRequest.Split("&".ToCharArray());
        foreach (string item in items)
        {
            string[] parts = item.Split("=".ToCharArray());
            nvs.Add(parts[0], HttpUtility.UrlDecode(parts[1]));
        }        

        //Post back to either sandbox or live
        //string url = "https://www.sandbox.paypal.com/cgi-bin/webscr";
        string url = https://www.paypal.com/cgi-bin/webscr; 
        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
        req.Method = "POST";
        req.ContentType = "application/x-www-form-urlencoded";

        //Append validation flag to the orginal transaction detail
        strRequest += "&cmd=_notify-validate";
        req.ContentLength = strRequest.Length;

        //Hook up 1&1 proxy server
        WebProxy proxy = new WebProxy(new Uri("http://ntproxy.1and1.com:3128"));
        req.Proxy = proxy;

        //Send transaction detail with the flag back to PayPal
        StreamWriter streamOut = new StreamWriter(req.GetRequestStream(), System.Text.Encoding.ASCII);
        streamOut.Write(strRequest);
        streamOut.Close();

        //Get validation status from PayPal
        StreamReader streamIn = new StreamReader(req.GetResponse().GetResponseStream());
        string strResponse = streamIn.ReadToEnd();
        streamIn.Close();

        //Send mail to payer or admin depending on validation status
        MailMessage mm = new MailMessage();
        mm.From = new MailAddress(ConfigurationManager.AppSettings["Sender"]);
        mm.IsBodyHtml = false;
        SmtpClient smtp = new SmtpClient();
        smtp.Host = ConfigurationManager.AppSettings["SmtpHost"];
        if (strResponse == "VERIFIED")
        {
            if (nvs["payment_status"] == "Completed")
            {
                mm.To.Add(nvs["payer_email"]);
                mm.Bcc.Add(ConfigurationManager.AppSettings["Sender"]);
                mm.Subject = string.Format("Thank you for you donation #{0}", nvs["txn_id"]);
                string body = string.Format("Dear {0},\n\nThank you for you donation of {1}{2}, paid on {3}. Transaction #: {4}\n\nSincerely,\n{5}", nvs["first_name"], nvs["mc_currency"], nvs["mc_gross"], nvs["payment_date"], nvs["txn_id"], ConfigurationManager.AppSettings["Sender"]);
                mm.Body = body;
                smtp.Send(mm);
            }
            else
            {
                mm.To.Add(ConfigurationManager.AppSettings["Sender"]);
                mm.Subject = "Incompleted transaction";
                mm.Body = strRequest;
                smtp.Send(mm);
            }
        }
        else if (strResponse == "INVALID")
        {
            mm.To.Add(ConfigurationManager.AppSettings["Sender"]);
            mm.Subject = "Invalid transaction";
            mm.Body = strRequest;
            smtp.Send(mm);
        }
        else
        {
            mm.To.Add(ConfigurationManager.AppSettings["Sender"]);
            mm.Subject = "Bad transaction";
            mm.Body = strRequest;
            smtp.Send(mm);
        }
    } 

The above code is adapted from this PayPal code sample. Here are some key points to note about the code. First, we use a generic Dictionary to store the transaction detail from PayPal in name-value pairs. Doing so makes it a lot easier to retrieve any value by transaction item name in the later part of the code. Second, to make sure if the transaction data is indeed from PayPal, we need to send it to PayPal for validation. To contact PayPal server from 1&1 server, we need to go through 1&1 proxy server. This is done by creating a WebProxy object with 1&1 proxy server's url (http://ntproxy.1and1.com) and port number (3128), and assigning it to the Proxy property of the HttpWebRequest object. Finally, based on the validation status from Paypal we need to take some actions. For this sample code, we just email a receipt to the client (donor) if the transaction is valid. Or we send the site admin an email if the transaction is invalid or some error has happened.

To see a live demo click here. Don't forget to donate some money if you find this post is useful Wink 

One Account, Multiple Sites

1&1 business account allows you to configure up to 5 ASP.Net applications in separate folders under the account's root folder. Each application can have its own web.config, Global.asax files and everything. If you have multiple domain names, you can associate one for each application so it looks like separate websites. By default all you domain names point to your account root folder. 1&1 has a feature called domain destination. With domain destination supposedly you could point a domain to an application folder and when you type the domain name in a browser, it should go straight to application's default page. But, somehow, this feature does not seem to work. A workaround is to place a default page in your account root folder and the default page redirect requests to different application folders based on their http host names. This configuration is essentially equivalent to one parent application with up to 5 child applications.

In the following example, I assume you have 3 web applications, app1, app2 and app3, each in its own folder, and 3 corresponding domain names, mysite1.com, mysite2.com and mysite3.com. First, you need to specify app1, app2 and app3 are application folders. Here are the steps to do this:

  1. Launch Webfiles utility
  2. Select File -> Application Settings menu item
  3. Click Create button
  4. Select app1 folder, and click OK button
  5. Repeat steps 3 & 4 for other application folders.

Next, deploy app1, app2 and app3 to corresponding app folders. 

Create the following default.aspx page:

<%@ Page Language="C#" %>
<%
    string host = Request.ServerVariables["HTTP_HOST"];
    if (host.ToLower().IndexOf("mysite1.com") >= 0)
        Response.Redirect("app1/");
    else if (host.ToLower().IndexOf("mysite2.com") >= 0)
        Response.Redirect("app2/");
    else if (host.ToLower().IndexOf("mysite3.com") >= 0)
        Response.Redirect("app3/");
    else
        Response.Write("Under construction");  //your all other domains
%>

Finally, upload the page to your account root folder. Now, if you type http://www.mysite1.com in a browser, you will get to http://www.mysite1.com/app1/default.aspx. Similarly, if you type in http://www.mysite2.com, you will get to http://www.mysite2.com/app2/default.aspx, etc. Of course, nothing prevents a user from typing in http://www.mysite2.com/app1/default.aspx to get to the app1's default page with the domain mysite2.com, if he knows all your domains and folder structures. 

Here are some extra notes: 

  • You can add a web.config file to your account root folder, and in this case all you child applications will inherite the configuration.
  • If you have an 1&1 dedicated SSL certificate for one of your domains, you can not shared it with your other domains, because the certificate is issued for a fixed domain name. If you try to use the SSL certificate with other domain, you will get the error "There is a problem with this website's security certificate."  
  • If you want to, you can use a default.asp page in place of the default.aspx page. Here is what the default.asp page looks like:

<%
    Dim host
    host = Request.ServerVariables("HTTP_HOST")
    if InStr(LCase(host), "mysite1.com") > 0 then
        Response.Redirect("app1/")
    elseif InStr(LCase(host), "mysite2.com") > 0 then
        Response.Redirect("app2/")
    elseif InStr(LCase(host), "mysite3.com") > 0 then
        Response.Redirect("app3/")
    else
        Response.Write("Under construction")
    end if
%>

About

A seasoned computer professional. A tofu culture evangelist...
more >>

Tag Cloud

Calendar

<<  April 2017  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar
Copyright © 2008-2011 Gong Liu. All rights reserved. | credits | contact me
The content on this site represents my own personal opinions, and does not reflect those of my employer in any way.