using System; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.Text; using ESRI.ArcGIS.Geometry; using System.Xml.Linq; using ESRI.ArcGIS; using System.Xml; using System.Diagnostics; using System.Collections.Generic; using System.Globalization; using System.Xml.XPath; namespace ESRI.ArcGIS.DataSources.WMS { /// /// A WMS service layer for the ArcGIS API for Microsoft Silverlight/WPF /// Original code from ESRI. /// Modifications by Christoph Lupprich - clupprich (at) gmail (dot) com. /// public class WMSMapServiceLayer : DynamicMapServiceLayer { List _layers; string _proxyUrl; BoundingExtent _boundingExtent = new BoundingExtent(); // Coordinate system WKIDs in WMS 1.3 where X,Y (Long,Lat) switched to Y,X (Lat,Long) private int[,] LatLongCRSRanges = new int[,] { { 4001, 4999 }, {2044, 2045}, {2081, 2083}, {2085, 2086}, {2093, 2093}, {2096, 2098}, {2105, 2132}, {2169, 2170}, {2176, 2180}, {2193, 2193}, {2200, 2200}, {2206, 2212}, {2319, 2319}, {2320, 2462}, {2523, 2549}, {2551, 2735}, {2738, 2758}, {2935, 2941}, {2953, 2953}, {3006, 3030}, {3034, 3035}, {3058, 3059}, {3068, 3068}, {3114, 3118}, {3126, 3138}, {3300, 3301}, {3328, 3335}, {3346, 3346}, {3350, 3352}, {3366, 3366}, {3416, 3416}, {20004, 20032}, {20064, 20092}, {21413, 21423}, {21473, 21483}, {21896, 21899}, {22171, 22177}, {22181, 22187}, {22191, 22197}, {25884, 25884}, {27205, 27232}, {27391, 27398}, {27492, 27492}, {28402, 28432}, {28462, 28492}, {30161, 30179}, {30800, 30800}, {31251, 31259}, {31275, 31279}, {31281, 31290}, {31466, 31700} }; public WMSMapServiceLayer() : base() { } #region Public Properties /// /// Required. Gets or sets the URL to a WMS service endpoint. /// For example, /// http://sampleserver1.arcgisonline.com/ArcGIS/services/Specialty/ESRI_StatesCitiesRivers_USA/MapServer/WMSServer, /// http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi. /// /// The URL. public string Url { get; set; } /// /// Required. Gets or sets the unique layer ids in a WMS service. /// Each id is a string value. At least one layer id must be defined. /// /// A string array of layer ids. [System.ComponentModel.TypeConverter(typeof(StringToStringArrayConverter))] public string[] Layers { get { return _layers.ToArray(); } set { _layers = new List(value); OnLayerChanged(); } } /// /// Optional. Gets or sets the URL to a proxy service that brokers Web requests between the Silverlight /// client and a WMS service. Use a proxy service when the WMS service is not hosted on a site that provides /// a cross domain policy file (clientaccesspolicy.xml or crossdomain.xml). You can also use a proxy to /// convert png images to a bit-depth that supports transparency in Silverlight. /// /// The proxy URL string. public string ProxyUrl { get { return _proxyUrl; } set { _proxyUrl = value; _proxyUrl += "?"; /* if (_proxyUrl.StartsWith("~")) { _proxyUrl = _proxyUrl.Replace("~", string.Format("{0}://{1}:{2}", Application.Current.Host.Source.Scheme, Application.Current.Host.Source.Host, Application.Current.Host.Source.Port)); } */ } } /// /// Optional. Gets or sets the WMS version. If SkipGetCapabilities property is set to true, this value determines version requested. /// If SkipGetCapabilities is false, this value determines version to retrieve. If no value specified, default value returned from /// the site will be used. /// /// The version string. public string Version { get; set; } /// /// Optional. Gets or sets a value indicating whether to skip a request to get capabilities. /// Default value is false. Set SkipGetCapabilities if the site hosting the WMS service does not provide a /// cross domain policy file and you do not have a proxy page. In this case, you must set the WMS service version. /// If true, the initial and full extent of the WMS Silverlight layer will not be defined. /// public bool SkipGetCapabilities { get; set; } #endregion private bool initializing; /// /// Initializes this a WMS layer. Calls GetCapabilities if SkipGetCapabilities is false. /// public override void Initialize() { if (initializing || IsInitialized) return; initializing = true; if (SkipGetCapabilities) { base.Initialize(); } else { string wmsUrl = string.Format("{0}{1}{2}{3}", ProxyUrl, Url, "?service=WMS&request=GetCapabilities&version=", Version); WebClient client = new WebClient(); client.DownloadStringCompleted += client_DownloadStringCompleted; client.DownloadStringAsync(new Uri(wmsUrl)); } } internal void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) { if (!CheckForError(e)) { // Will be set when capabilities processed InitialExtent = new ESRI.ArcGIS.Geometry.Envelope(); XmlDocument doc = new XmlDocument(); doc.LoadXml(e.Result); processDocument(doc); // Define full extent when all included layers are processed FullExtent = new Envelope(_boundingExtent.XMin, _boundingExtent.XMax, _boundingExtent.YMin, _boundingExtent.YMax); } // Call initialize regardless of error base.Initialize(); } #region Process Capabilities private void processDocument(XmlDocument doc) { XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable); nsManager.AddNamespace("wms", "http://www.opengis.net/wms"); XmlNode capabilities = doc.SelectSingleNode("WMT_MS_Capabilities|wms:WMS_Capabilities", nsManager); Version = capabilities.Attributes["version"].Value; Debug.WriteLine(String.Format("WMS Version is {0}", Version)); XmlNodeList layers = doc.SelectNodes("//Layer|//wms:Layer", nsManager); foreach (XmlNode layer in layers) { string name = layer.SelectSingleNode("Name|wms:Name", nsManager).InnerText; if (!_layers.Contains(name)) continue; XmlNode bbox = layer.SelectSingleNode("BoundingBox|wms:BoundingBox", nsManager); InitialExtent = getBoundingBox(bbox); _boundingExtent.XMin = InitialExtent.XMin; _boundingExtent.YMin = InitialExtent.YMin; _boundingExtent.XMax = InitialExtent.XMax; _boundingExtent.YMax = InitialExtent.YMax; } } private Envelope getBoundingBox(XmlNode node) { Envelope envelope = new Envelope(); switch (Version) { case "1.3.0": envelope.SpatialReference = new SpatialReference(int.Parse(node.Attributes["CRS"].Value.Split(':')[1])); envelope.XMin = Double.Parse(node.Attributes["minx"].Value, CultureInfo.InvariantCulture); envelope.YMin = Double.Parse(node.Attributes["miny"].Value, CultureInfo.InvariantCulture); envelope.XMax = Double.Parse(node.Attributes["maxx"].Value, CultureInfo.InvariantCulture); envelope.YMax = Double.Parse(node.Attributes["maxy"].Value, CultureInfo.InvariantCulture); break; case "1.1.1": envelope.SpatialReference = new SpatialReference(int.Parse(node.Attributes["SRS"].Value.Split(':')[1])); envelope.XMin = Double.Parse(node.Attributes["minx"].Value, CultureInfo.InvariantCulture); envelope.YMin = Double.Parse(node.Attributes["miny"].Value, CultureInfo.InvariantCulture); envelope.XMax = Double.Parse(node.Attributes["maxx"].Value, CultureInfo.InvariantCulture); envelope.YMax = Double.Parse(node.Attributes["maxy"].Value, CultureInfo.InvariantCulture); break; } return envelope; } #endregion private bool CheckForError(DownloadStringCompletedEventArgs e) { if (e.Cancelled) { InitializationFailure = new Exception("Request Cancelled"); return true; } if (e.Error != null) { Exception ex = e.Error; if (ex is System.Security.SecurityException) { ex = new System.Security.SecurityException( @"A security exception occured while trying to connect to the WMS service. Make sure you have a cross domain policy file available at the root for your server that allows for requests from this application. If not, use a proxy page (handler) to broker communication.", ex); } InitializationFailure = ex; return true; } return false; } /// /// Gets the URL. Override from DynamicMapServiceLayer /// /// The extent. /// The width. /// The height. /// OnUrlComplete delegate. /// /// The Map has a private method loadLayerInView which calls Layer.Draw. /// The DynamicMapServiceLayer abstract class overrides the Draw method and calls /// DynamicMapServiceLayer.GetUrl which must be implemented in a subclass. /// The last parameter is the OnUrlComplete delegate, which is used to pass the appropriate values /// (url, width, height, envelope) to the private DynamicMapServiceLayer.getUrlComplete method. /// public override void GetUrl(ESRI.ArcGIS.Geometry.Envelope extent, int width, int height, DynamicMapServiceLayer.OnUrlComplete onComplete) { if (width == 0 && height == 0) return; int extentWKID = extent.SpatialReference.WKID; StringBuilder mapURL = new StringBuilder(); mapURL.Append(ProxyUrl + Url); mapURL.Append("?service=WMS"); mapURL.Append("&request=GetMap"); mapURL.AppendFormat("&width={0}", width); mapURL.AppendFormat("&height={0}", height); mapURL.AppendFormat("&format={0}", "image/png"); mapURL.AppendFormat("&layers={0}", String.Join(",", Layers)); mapURL.Append("&styles="); mapURL.AppendFormat("&bgcolor={0}", "0xFFFFFF"); mapURL.AppendFormat("&transparent={0}", "true"); mapURL.AppendFormat("&version={0}", Version); switch (Version) { case ("1.1.1"): mapURL.AppendFormat("&SRS=EPSG:{0}", extentWKID); mapURL.AppendFormat(CultureInfo.InvariantCulture, "&bbox={0},{1},{2},{3}", extent.XMin, extent.YMin, extent.XMax, extent.YMax); break; case ("1.3"): case ("1.3.0"): mapURL.AppendFormat("&CRS=EPSG:{0}", extentWKID); bool useLatLong = false; int length = LatLongCRSRanges.Length / 2; for (int count = 0; count < length; count++) { if (extentWKID >= LatLongCRSRanges[count, 0] && extentWKID <= LatLongCRSRanges[count, 1]) { useLatLong = true; break; } } if (useLatLong) mapURL.AppendFormat(CultureInfo.InvariantCulture, "&bbox={0},{1},{2},{3}", extent.YMin, extent.XMin, extent.YMax, extent.XMax); else mapURL.AppendFormat(CultureInfo.InvariantCulture, "&bbox={0},{1},{2},{3}", extent.XMin, extent.YMin, extent.XMax, extent.YMax); break; } onComplete(mapURL.ToString(), width, height, new ESRI.ArcGIS.Geometry.Envelope() { XMin = extent.XMin, YMin = extent.YMin, XMax = extent.XMax, YMax = extent.YMax }); } internal class BoundingExtent { double _xmin = double.MaxValue; double _ymin = double.MaxValue; double _xmax = double.MinValue; double _ymax = double.MinValue; public double XMin { get { return _xmin; } set { if (value < _xmin) _xmin = value; } } public double YMin { get { return _ymin; } set { if (value < _ymin) _ymin = value; } } public double XMax { get { return _xmax; } set { if (value > _xmax) _xmax = value; } } public double YMax { get { return _ymax; } set { if (value > _ymax) _ymax = value; } } } /// /// String To String Array Converter /// public sealed class StringToStringArrayConverter : System.ComponentModel.TypeConverter { public override bool CanConvertFrom(System.ComponentModel.ITypeDescriptorContext context, Type sourceType) { return (sourceType == typeof(string)); } public override object ConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { if (value == null) return null; if (value is string) { string[] values = (value as string).Replace(" ", "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); if (values.Length == 0) return null; return values; } throw new NotSupportedException("Cannot convert to string array"); } } } }