Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Weather Radar

4.95/5 (7 votes)
5 Dec 2014CPOL2 min read 37.9K   751  
A Bare-Bones Series Article

Image 1

Code Download

This project requires that you download the HtmlAgilityPack separately and place the HtmlAgilityPack.dll into the appropriate Debug and/or Release folder.

Introduction

As far as I could tell, the NOAA weather webservice does not provide a function for obtaining the weather radar image of particular location, nor is this feature readily provided by other weather web services.  So here I present a "bare-bones" implementation, using HtmlAgilityPack for some simple XPath queries.

The radar.weather.gov Website

If you inspect a page displaying a radar image on this website, you'll notice that the image is actually a composite of 8 selectable pieces:

Image 2

The first image is a JPG, and the remaining are GIF's:

Image 3

Topography

Image 4

Doppler Radar

Image 5

Counties

Image 6

Rivers

Image 7

Roads

Image 8

Cities

Warnings (none at this moment)

Image 9

Legend

So, given you're nearest radar station (pick one on http://radar.weather.gov) we need to obtain these images and combine them together to show the final composite image.  There's a slight twist - two of the images come from a different source and are computed by the Javascript on the page, so we have special handlers for these.  There are five steps:

Step 1: Acquiring the Page HTML

Given the URL entered in the textbox, we first acquire the HTML for the page:

protected string GetPageHtml(string url)
{
  using (WebClient client = new WebClient())
  {
    return client.DownloadString(url);
  }
}

Step 2: Figuring out the Image URL's

This is the most complex part of the process, as we have to programmatically generate two of the image URL's.  You'll also see how we use the HtmlAgilityPack to extract the specific node values for the div sections you saw earlier.

protected List<string> GetPageImageUrls(string html, string url)
{
  List<string> ret = new List<string>();

  HtmlAgilityPack.HtmlDocument doc = new HtmlAgilityPack.HtmlDocument();
  doc.LoadHtml(html);
  WebClient webClient = new WebClient();
  int n = 0;

  while (true)
  {
    var nodes = doc.DocumentNode.SelectNodes(String.Format("//div[@id='image{0}']/img", n++));

    if ((nodes != null) && (nodes.Count == 1))
    {
      string imageUrl = nodes.Select(node => node.Attributes["src"].Value).Single();

      // This is a computed image.
      if (imageUrl == "#")
      {
        string name = nodes.Select(node => node.Attributes["name"].Value).Single();
        string rid = url.Between("rid=", "&").ToUpper();
        string product = url.Between("product=", "&").ToUpper();

        switch (name)
        {
          case "conditionalimage":
          // Example: <a href="http://radar.weather.gov/RadarImg/N0R/ENX_N0R_0.gif">http://radar.weather.gov/RadarImg/N0R/ENX_N0R_0.gif</a>
          imageUrl = String.Format("RadarImg/{0}/{1}_{0}_0.gif", product, rid);
          break;

        case "conditionallegend":
          // Example: <a href="http://radar.weather.gov/Legend/N0R/ENX_N0R_Legend_0.gif">http://radar.weather.gov/Legend/N0R/ENX_N0R_Legend_0.gif</a>
          imageUrl = String.Format("Legend/{0}/{1}_{0}_Legend_0.gif", product, rid);
          break;
        }
      }

      ret.Add(imageUrl);
    }
    else
    {
      break;
    }
  }

  webClient.Dispose();

  return ret;
}

Step 3: Downloading the Images

Here we simply download the images, saving the Image and its associated MemoryStream instance.  While I could clone the image at this point and free up the memory stream and the original image, I opted to keep the two together as a package for later cleanup.

protected List<ImageData> DownloadImages(List<string> imageUrls)
{
  List<ImageData> ret = new List<ImageData>();
  WebClient webClient = new WebClient();

  foreach (string url in imageUrls)
  {
    byte[] data = webClient.DownloadData("http://radar.weather.gov/" + url);
    // Memory stream CANNOT be disposed of!
    MemoryStream stream = new MemoryStream(data);
    Image image = Image.FromStream(stream);
    ret.Add(new ImageData() { Image = image, BackingStream = stream });
  }

  webClient.Dispose();

  return ret;
}

Step 4: For fun, we Save the Images

You'll note my assumption that the first image is a JPG and the rest are GIF's.  Yes, we could figure this out by inspecting the raw image format, but that's more work than I wanted to put into the code at the moment.

protected void WriteImages(List<ImageData> images)
{
  int n=0;
  images.ForEach(img => img.Image.Save("img" + n++ + (n==1 ? ".jpg" : ".gif")));
}

Step 5: Combine the Images

Here we combine the images, returning a new image consisting of the composite image data:

protected Image CombineImages(Graphics gr, List<ImageData> images, Size size)
{
  Image baseImage = (Image)images[0].Image.Clone();
  gr = Graphics.FromImage(baseImage);

  for (int i=1; i<images.Count; i++)
  {
    gr.DrawImage(images[i].Image, new Point(0, 0));
  }

  gr.Dispose();

  return baseImage;
}

Step 6: Cleanup

Here we dispose of the images and their the associated memory streams.

protected void Cleanup(List<ImageData> images)
{
  images.ForEach(img =>
  {
    img.BackingStream.Dispose();
    img.Image.Dispose();
  });
}

Putting it all Together

When you click on the Go button, an async process is started, and we provide a callback to display the progress on the status bar:

protected async void btnGo_Click(object sender, EventArgs e)
{
  btnGo.Enabled = false;
  string url = tbUrl.Text;
  Graphics gr = pbRadar.CreateGraphics();
  Image bitmap = await Task.Run(() => GetRadarImage(gr, 
    url, 
    (progress) => this.BeginInvoke(() => tsLabel.Text = progress)));
  pbRadar.Image = bitmap;
  tsLabel.Text = "Done";
  btnGo.Enabled = true;
}

And the implementation of GetRadarImage chains together the processes we described above:

protected Image GetRadarImage(Graphics gr, string url, Action<string> progressCallback)
{
  progressCallback("Acquiring page...");
  string html = GetPageHtml(url);

  progressCallback("Scraping page...");
  List<string> imageUrls = GetPageImageUrls(html, url);

  progressCallback("Downloading images...");
  List<ImageData> images = DownloadImages(imageUrls);

  progressCallback("Writing images...");
  WriteImages(images);

  progressCallback("Combining images...");
  Image bitmap = CombineImages(gr, images, pbRadar.Size);


  progressCallback("Cleanup...");
  Cleanup(images);

  return bitmap;
}

Extension Methods

To make my life a bit easier, I borrowed some extension methods that I use in other projects:

public static class Extensions
{
  public static string Between(this String src, string s1, string s2)
  {
    return src.RightOf(s1).LeftOf(s2);
  }

  public static string RightOf(this String src, string s)
  {
    string ret = String.Empty;
    int idx = src.IndexOf(s);

    if (idx != -1)
    {
      ret = src.Substring(idx + s.Length);
    }

    return ret;
  }

  public static string LeftOf(this String src, string s)
  {
    string ret = src;
    int idx = src.IndexOf(s);
  
    if (idx != -1)
    {
      ret = src.Substring(0, idx);
    }

    return ret;
  }

  public static void BeginInvoke(this Control control, Action action)
  {
    if (control.InvokeRequired)
    {
      control.BeginInvoke((Delegate)action);
    }
    else
    {
      action();
    }
  }
}

Conclusion

And there you have it, a bare-bones approach to acquiring a radar image in your own application!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)