天气雷达






4.95/5 (7投票s)
一个精简版系列文章。
代码下载
这个项目需要您单独下载 HtmlAgilityPack 并将 HtmlAgilityPack.dll 放置在适当的 Debug 和/或 Release 文件夹中。
引言
据我所知,NOAA 天气网络服务没有提供获取特定位置天气雷达图像的功能,其他天气网络服务也没有轻易提供此功能。因此,我在这里展示一个“最简实现”,使用 HtmlAgilityPack 进行一些简单的 XPath 查询。
radar.weather.gov 网站
如果您检查此网站上显示雷达图像的页面,您会注意到该图像实际上是 8 个可选择部分的组合。
第一张图像是 JPG,其余都是 GIF。
地形 |
多普勒雷达 |
县 |
河流 |
道路 |
Cities |
预警(目前没有) |
图例 |
因此,给定您最近的雷达站(在 http://radar.weather.gov 上选择一个),我们需要获取这些图像并将它们组合在一起以显示最终的合成图像。有一个小小的变化 - 两张图像来自不同的来源,并且由页面上的 Javascript 计算,因此我们为这些图像提供了特殊的处理程序。有五个步骤:
步骤 1:获取页面 HTML
给定文本框中输入的 URL,我们首先获取页面的 HTML。
protected string GetPageHtml(string url) { using (WebClient client = new WebClient()) { return client.DownloadString(url); } }
步骤 2:确定图像 URL
这是流程中最复杂的部分,因为我们需要以编程方式生成两个图像 URL。您还将看到我们如何使用 HtmlAgilityPack 提取您之前看到的 div
部分的特定节点值。
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: http://radar.weather.gov/RadarImg/N0R/ENX_N0R_0.gif imageUrl = String.Format("RadarImg/{0}/{1}_{0}_0.gif", product, rid); break; case "conditionallegend": // Example: http://radar.weather.gov/Legend/N0R/ENX_N0R_Legend_0.gif imageUrl = String.Format("Legend/{0}/{1}_{0}_Legend_0.gif", product, rid); break; } } ret.Add(imageUrl); } else { break; } } webClient.Dispose(); return ret; }
步骤 3:下载图像
在这里,我们只需下载图像,保存 Image
及其关联的 MemoryStream
实例。虽然我可以克隆图像并在此时释放内存流和原始图像,但我选择将两者作为一个包保留以供以后清理。
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; }
步骤 4:为了好玩,我们保存图像
您会注意到我对第一张图像是 JPG,其余都是 GIF 的假设。是的,我们可以通过检查原始图像格式来弄清楚这一点,但这比我想投入到代码中的工作量要多。
protected void WriteImages(List<ImageData> images) { int n=0; images.ForEach(img => img.Image.Save("img" + n++ + (n==1 ? ".jpg" : ".gif"))); }
步骤 5:组合图像
在这里,我们将图像组合在一起,返回包含合成图像数据的新的图像。
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; }
步骤 6:清理
在这里,我们处置图像及其关联的内存流。
protected void Cleanup(List<ImageData> images) { images.ForEach(img => { img.BackingStream.Dispose(); img.Image.Dispose(); }); }
整合起来
当您单击“Go”按钮时,将启动一个异步过程,我们提供一个回调来在状态栏上显示进度。
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; }
GetRadarImage 的实现将我们描述的流程连接在一起。
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; }
扩展方法
为了让我的生活更轻松,我借用了一些我在其他项目中使用过的扩展方法。
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(); } } }
结论
就这样,一种获取应用程序中雷达图像的最简方法!