From ddf98b1e20efb3d8dbd110f23fb5b75a943552bf Mon Sep 17 00:00:00 2001 From: Dave Timmins Date: Fri, 12 Jan 2018 07:50:24 +1300 Subject: [PATCH 1/3] added ExportMap operation --- NUGET-DOC.md | 1 + README.md | 1 + build.cake | 2 +- src/Anywhere.ArcGIS/Anywhere.ArcGIS.csproj | 9 +- .../Operation/CommonParameters.cs | 1 + src/Anywhere.ArcGIS/Operation/ExportMap.cs | 184 ++++++++++++++++++ src/Anywhere.ArcGIS/Operation/Query.cs | 1 + src/Anywhere.ArcGIS/PortalGatewayBase.cs | 61 ++++++ .../ArcGISGatewayTests.cs | 61 +++++- 9 files changed, 313 insertions(+), 8 deletions(-) create mode 100644 src/Anywhere.ArcGIS/Operation/ExportMap.cs diff --git a/NUGET-DOC.md b/NUGET-DOC.md index 3de1d78..bbd8ed9 100644 --- a/NUGET-DOC.md +++ b/NUGET-DOC.md @@ -47,6 +47,7 @@ Supports the following as typed operations: - `DescribeLayer` return layer information - `HealthCheck` verify that the server is accepting requests - `GetFeature` return a feature from a map/feature service + - `ExportMap` get an image (or url to the image) of a service REST admin operations: diff --git a/README.md b/README.md index 5ae498f..227583b 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Supports the following as typed operations: - `DescribeLayer` return layer information - `HealthCheck` verify that the server is accepting requests - `GetFeature` return a feature from a map/feature service + - `ExportMap` get an image (or url to the image) of a service REST admin operations: diff --git a/build.cake b/build.cake index c85f818..1e9ab4c 100644 --- a/build.cake +++ b/build.cake @@ -9,7 +9,7 @@ var target = Argument("target", "Default"); var configuration = Argument("configuration", "Release"); var solution = "./Anywhere.ArcGIS.sln"; -var version = "1.1.0"; +var version = "1.2.0"; var versionSuffix = Environment.GetEnvironmentVariable("VERSION_SUFFIX"); ////////////////////////////////////////////////////////////////////// diff --git a/src/Anywhere.ArcGIS/Anywhere.ArcGIS.csproj b/src/Anywhere.ArcGIS/Anywhere.ArcGIS.csproj index e9e6266..083f325 100644 --- a/src/Anywhere.ArcGIS/Anywhere.ArcGIS.csproj +++ b/src/Anywhere.ArcGIS/Anywhere.ArcGIS.csproj @@ -15,8 +15,7 @@ https://github.com/davetimmins/Anywhere.ArcGIS git ArcGIS ArcGISServer ArcGISOnline Esri REST netstandard anywhere GIS Mapping Map Location GeoLocation OAuth - Initial port of ArcGIS.PCL to netstandard - 1.1.0 + 1.2.0 @@ -25,8 +24,8 @@ - - - + + + diff --git a/src/Anywhere.ArcGIS/Operation/CommonParameters.cs b/src/Anywhere.ArcGIS/Operation/CommonParameters.cs index 9c0e469..364fe64 100644 --- a/src/Anywhere.ArcGIS/Operation/CommonParameters.cs +++ b/src/Anywhere.ArcGIS/Operation/CommonParameters.cs @@ -21,6 +21,7 @@ public static class Operations public const string Simplify = "simplify"; public const string Buffer = "buffer"; public const string Project = "project"; + public const string ExportMap = "export"; public const string PublicKey = "publicKey"; public const string ServiceStatus = "services/{0}.{1}/status"; public const string StartService = "services/{0}.{1}/start"; diff --git a/src/Anywhere.ArcGIS/Operation/ExportMap.cs b/src/Anywhere.ArcGIS/Operation/ExportMap.cs new file mode 100644 index 0000000..97b1355 --- /dev/null +++ b/src/Anywhere.ArcGIS/Operation/ExportMap.cs @@ -0,0 +1,184 @@ +using Anywhere.ArcGIS.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; + +namespace Anywhere.ArcGIS.Operation +{ + [DataContract] + public class ExportMap : ArcGISServerOperation + { + public ExportMap(string relativeUrl, Action beforeRequest = null, Action afterRequest = null) + : this(relativeUrl.AsEndpoint(), beforeRequest, afterRequest) + { } + + /// + /// Requests an export of the map resources. Returns the image link in the response + /// + /// Resource to apply the export against + public ExportMap(ArcGISServerEndpoint endpoint, Action beforeRequest = null, Action afterRequest = null) + : base(endpoint.RelativeUrl.Trim('/') + "/" + Operations.ExportMap, beforeRequest, afterRequest) + { + Size = new List { 400, 400 }; + Dpi = 96; + ImageFormat = "png"; + } + + /// + /// (Required) The extent (bounding box) of the exported image. + /// Unless the bboxSR parameter has been specified, the bbox is assumed to be in the spatial reference of the map. + /// + [IgnoreDataMember] + public Extent ExportExtent { get; set; } + + /// + /// (Required) The extent (bounding box) of the exported image. + /// Unless the bboxSR parameter has been specified, the bbox is assumed to be in the spatial reference of the map. + /// + [DataMember(Name = "bbox")] + public string ExportExtentBoundingBox + { + get + { + return ExportExtent == null ? + string.Empty : + string.Format("{0},{1},{2},{3}", ExportExtent.XMin, ExportExtent.YMin, ExportExtent.XMax, ExportExtent.YMax); + } + } + + /// + /// The spatial reference of the ExportExtentBoundingBox. + /// + [DataMember(Name = "bboxSR")] + public SpatialReference ExportExtentBoundingBoxSpatialReference + { + get { return ExportExtent == null ? null : ExportExtent.SpatialReference ?? null; } + } + + /// + /// The size (width * height) of the exported image in pixels. + /// If the size is not specified, an image with a default size of 400 * 400 will be exported. + /// + [IgnoreDataMember] + public List Size { get; set; } + + /// + /// The size width *height) of the exported image in pixels. + /// If the size is not specified, an image with a default size of 400 * 400 will be exported. + /// + [DataMember(Name = "size")] + public string SizeValue + { + get + { + if (Size?.Count > 0 && Size?.Count != 2) + { + throw new ArgumentOutOfRangeException(nameof(Size), "If you want to use the SizeRange parameter then you need to supply exactly 2 values: the lower and upper range"); + } + + return Size == null || !Size.Any() ? null : string.Join(",", Size); + } + } + + /// + /// The device resolution of the exported image (dots per inch). + /// If the dpi is not specified, an image with a default DPI of 96 will be exported. + /// + [DataMember(Name = "dpi")] + public int Dpi { get; set; } + + /// + /// The spatial reference of the exported image. + /// + [DataMember(Name = "imageSR")] + public SpatialReference ImageSpatialReference { get; set; } + + /// + /// The format of the exported image. The default format is png. + /// Values: png | png8 | png24 | jpg | pdf | bmp | gif | svg | svgz | emf | ps | png32 + /// + [DataMember(Name = "format")] + public string ImageFormat { get; set; } + + /// + /// Allows you to filter the features of individual layers in the exported map by specifying definition expressions for those layers. + /// Definition expression for a layer that is published with the service will be always honored. + /// + [DataMember(Name = "layerDefs")] + public Dictionary LayerDefinitions { get; set; } + + /// + /// If true, the image will be exported with the background color of the map set as its transparent color. + /// The default is false. + /// Only the .png and .gif formats support transparency. + /// Internet Explorer 6 does not display transparency correctly for png24 image formats. + /// + [DataMember(Name = "transparent")] + public bool TransparentBackground { get; set; } + + [IgnoreDataMember] + public DateTime? From { get; set; } + + [IgnoreDataMember] + public DateTime? To { get; set; } + + /// + /// The time instant or the time extent to query. + /// + /// If no To value is specified we will use the From value again, equivalent of using a time instant. + [DataMember(Name = "time")] + public string Time + { + get + { + return (From == null) ? null : string.Format("{0},{1}", + From.Value.ToUnixTime(), + (To ?? From.Value).ToUnixTime()); + } + } + + /// + /// GeoDatabase version to export from. + /// + [DataMember(Name = "gdbVersion")] + public string GeodatabaseVersion { get; set; } + + /// + /// Use this parameter to export a map image at a specific scale, with the map centered around the center of the specified bounding box (bbox). + /// + [DataMember(Name = "mapScale")] + public long MapScale { get; set; } + + /// + /// Use this parameter to export a map image rotated at a specific angle, with the map centered around the center of the specified bounding box (bbox). + /// It could be positive or negative number. + /// + [DataMember(Name = "rotation")] + public int Rotation { get; set; } + } + + [DataContract] + public class ExportMapResponse : PortalResponse + { + [DataMember(Name = "href")] + public string ImageUrl { get; set; } + + [DataMember(Name = "width")] + public int Width { get; set; } + + [DataMember(Name = "height")] + public int Height { get; set; } + + [DataMember(Name = "extent")] + public Extent Extent { get; set; } + + [DataMember(Name = "scale")] + public double Scale { get; set; } + + public string ImageFormat { get { return string.IsNullOrEmpty(ImageUrl) ? string.Empty : ImageUrl.Substring(ImageUrl.LastIndexOf(".") + 1); } } + + public SpatialReference ImageSpatialReference { get { return Extent?.SpatialReference; } } + } +} diff --git a/src/Anywhere.ArcGIS/Operation/Query.cs b/src/Anywhere.ArcGIS/Operation/Query.cs index 4a14b93..adec2db 100644 --- a/src/Anywhere.ArcGIS/Operation/Query.cs +++ b/src/Anywhere.ArcGIS/Operation/Query.cs @@ -52,6 +52,7 @@ public SpatialReference InputSpatialReference { get { return Geometry == null ? null : Geometry.SpatialReference ?? null; } } + /// /// The type of geometry specified by the geometry parameter. /// The geometry type can be an envelope, point, line, or polygon. diff --git a/src/Anywhere.ArcGIS/PortalGatewayBase.cs b/src/Anywhere.ArcGIS/PortalGatewayBase.cs index 898ce60..47aeb8c 100644 --- a/src/Anywhere.ArcGIS/PortalGatewayBase.cs +++ b/src/Anywhere.ArcGIS/PortalGatewayBase.cs @@ -472,6 +472,25 @@ public virtual async Task DownloadAttachmentToLocal(Attachment attachm return Get(queryDomains, ct); } + /// + /// The export operation is performed on a map service resource. + /// The result of this operation is a map image resource. + /// This resource provides information about the exported map image such as its URL, its width and height, extent and scale. + /// + /// Note that the extent displayed in the exported map image may not exactly match the extent sent in the bbox parameter when the aspect ratio of the image size does not match the aspect ratio of the bbox. + /// The aspect ratio is the height divided by the width. + /// In these cases the extent is re-sized to prevent map images from appearing stretched. + /// The exported map's extent is sent along with the response and may be used in client side calculations. + /// So it is important that the client-side code update its extent based on the response. + /// + /// + /// + /// + public virtual Task ExportMap(ExportMap exportMap, CancellationToken ct = default(CancellationToken)) + { + return Get(exportMap, ct); + } + async Task CheckGenerateToken(CancellationToken ct) { if (TokenProvider == null) @@ -489,6 +508,48 @@ async Task CheckGenerateToken(CancellationToken ct) return token; } + /// + /// + /// + /// + /// + /// If not specified a Guid will be used for the name + /// + public virtual async Task DownloadExportMapToLocal(ExportMapResponse exportMapResponse, string folderLocation, string fileName = null) + { + LiteGuard.Guard.AgainstNullArgument(nameof(exportMapResponse), exportMapResponse); + + if (string.IsNullOrWhiteSpace(exportMapResponse.ImageUrl)) + { + throw new ArgumentNullException(nameof(exportMapResponse.ImageUrl)); + } + + if (string.IsNullOrWhiteSpace(folderLocation)) + { + throw new ArgumentNullException(nameof(folderLocation)); + } + + if (string.IsNullOrWhiteSpace(fileName)) + { + fileName = Guid.NewGuid().ToString(); + } + + var response = await _httpClient.GetAsync(exportMapResponse.ImageUrl); + response.EnsureSuccessStatusCode(); + await response.Content.LoadIntoBufferAsync(); + + var fileInfo = new FileInfo(Path.Combine(folderLocation, $"{fileName}.{exportMapResponse.ImageFormat}")); + + using (var fileStream = new FileStream(fileInfo.FullName, FileMode.Create, FileAccess.Write, FileShare.None)) + { + await response.Content.CopyToAsync(fileStream); + } + + _logger.DebugFormat("Saved export map response to {0}.", fileInfo.FullName); + + return new FileInfo(fileInfo.FullName); + } + void CheckRefererHeader(string referrer) { if (_httpClient == null || string.IsNullOrWhiteSpace(referrer)) diff --git a/tests/Anywhere.ArcGIS.Test.Integration/ArcGISGatewayTests.cs b/tests/Anywhere.ArcGIS.Test.Integration/ArcGISGatewayTests.cs index be203b8..c4ff6e8 100644 --- a/tests/Anywhere.ArcGIS.Test.Integration/ArcGISGatewayTests.cs +++ b/tests/Anywhere.ArcGIS.Test.Integration/ArcGISGatewayTests.cs @@ -695,7 +695,7 @@ public async Task CanQueryAttachments() Assert.True(result.AttachmentGroups.Any()); var group = result.AttachmentGroups.First(); - Assert.Equal(group.ParentGlobalId, queryAttachments.GlobalIds.First()); + Assert.Equal(group.ParentGlobalId, queryAttachments.GlobalIds.First()); Assert.Equal(group.AttachmentInfos.First().ContentType, queryAttachments.AttachmentTypes); } @@ -725,7 +725,7 @@ public async Task CanQueryDomains() public async Task CanGetHealthCheck(string rootUrl) { var gateway = new PortalGateway(rootUrl); - + var result = await IntegrationTestFixture.TestPolicy.ExecuteAsync(() => { return gateway.HealthCheck(); @@ -756,5 +756,62 @@ public async Task CanGetFeature(string rootUrl, string relativeUrl, long objectI Assert.NotNull(result.Feature.Geometry); Assert.Equal(result.Feature.ObjectID, objectId); } + + [Theory] + [InlineData("https://services.arcgisonline.com/arcgis", "Ocean/World_Ocean_Base/MapServer")] + public async Task CanExportMap(string rootUrl, string relativeUrl) + { + var gateway = new PortalGateway(rootUrl); + + var result = await IntegrationTestFixture.TestPolicy.ExecuteAsync(() => + { + return gateway.ExportMap(new ExportMap(relativeUrl) + { + ExportExtent = new Extent + { + XMin = -28695213.908633016, + YMin = -32794.530181307346, + XMax = 28695213.908633016, + YMax = 19971868.880408566, + SpatialReference = SpatialReference.WebMercator + } + }); + }); + + Assert.NotNull(result); + Assert.Null(result.Error); + Assert.NotNull(result.ImageUrl); + } + + [Theory] + [InlineData("https://services.arcgisonline.com/arcgis/", "Ocean/World_Ocean_Base/MapServer")] + public async Task CanDownloadExportMapResponse(string rootUrl, string relativeUrl) + { + var gateway = new PortalGateway(rootUrl); + + var exportMapResult = await IntegrationTestFixture.TestPolicy.ExecuteAsync(() => + { + return gateway.ExportMap(new ExportMap(relativeUrl) + { + ExportExtent = new Extent + { + XMin = -28695213.908633016, + YMin = -32794.530181307346, + XMax = 28695213.908633016, + YMax = 19971868.880408566, + SpatialReference = SpatialReference.WebMercator + } + }); + }); + + var result = await IntegrationTestFixture.TestPolicy.ExecuteAsync(() => + { + return gateway.DownloadExportMapToLocal(exportMapResult, @"c:\temp\_tests_"); + }); + + Assert.NotNull(result); + Assert.NotNull(result.FullName); + Assert.True(result.Exists); + } } } From de141e45798825c5742dcad1dfa64a8229a6a50a Mon Sep 17 00:00:00 2001 From: Dave Timmins Date: Fri, 12 Jan 2018 12:24:45 +1300 Subject: [PATCH 2/3] add more create gateway examples --- NUGET-DOC.md | 7 +++++++ README.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/NUGET-DOC.md b/NUGET-DOC.md index bbd8ed9..10bb270 100644 --- a/NUGET-DOC.md +++ b/NUGET-DOC.md @@ -4,6 +4,13 @@ Create an instance of that by specifying the root url of your server. The format ```c# var gateway = new PortalGateway("https://sampleserver3.arcgisonline.com/ArcGIS/"); + +// If you want to access secure resources then pass in a username / password +// this assumes the token service is in the default location for the ArcGIS Server +var secureGateway = new PortalGateway("https://sampleserver3.arcgisonline.com/ArcGIS/", "username", "password"); + +// Or use the static Create method which will discover the token service Url from the server Info endpoint +var autoTokenProviderLocationGateway = await PortalGateway.Create("https://sampleserver3.arcgisonline.com/ArcGIS/", "username", "password"); ``` Now you have access to the various operations supported by it. For example to call a query against a service diff --git a/README.md b/README.md index 227583b..a391859 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,13 @@ Create an instance of that by specifying the root url of your server. The format ```c# var gateway = new PortalGateway("https://sampleserver3.arcgisonline.com/ArcGIS/"); + +// If you want to access secure resources then pass in a username / password +// this assumes the token service is in the default location for the ArcGIS Server +var secureGateway = new PortalGateway("https://sampleserver3.arcgisonline.com/ArcGIS/", "username", "password"); + +// Or use the static Create method which will discover the token service Url from the server Info endpoint +var autoTokenProviderLocationGateway = await PortalGateway.Create("https://sampleserver3.arcgisonline.com/ArcGIS/", "username", "password"); ``` Now you have access to the various operations supported by it. For example to call a query against a service From e17f82344516811ed638eb291863a036012ad016 Mon Sep 17 00:00:00 2001 From: Dave Timmins Date: Tue, 16 Jan 2018 07:42:32 +1300 Subject: [PATCH 3/3] don't escape if string is too long #10 --- src/Anywhere.ArcGIS/Extensions/StringExtensions.cs | 6 +++++- .../GeometryGatewayTests.cs | 11 +++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Anywhere.ArcGIS/Extensions/StringExtensions.cs b/src/Anywhere.ArcGIS/Extensions/StringExtensions.cs index 8573677..eeb6664 100644 --- a/src/Anywhere.ArcGIS/Extensions/StringExtensions.cs +++ b/src/Anywhere.ArcGIS/Extensions/StringExtensions.cs @@ -58,7 +58,11 @@ public static string AsRootUrl(this string rootUrl) public static string UrlEncode(this string text) { - return string.IsNullOrWhiteSpace(text) ? text : Uri.EscapeDataString(text); + return string.IsNullOrWhiteSpace(text) + ? text + : text.Length > 65520 + ? text // this will get sent to POST anyway so don't bother escaping + : Uri.EscapeDataString(text); } /// diff --git a/tests/Anywhere.ArcGIS.Test.Integration/GeometryGatewayTests.cs b/tests/Anywhere.ArcGIS.Test.Integration/GeometryGatewayTests.cs index 7cb7531..921632c 100644 --- a/tests/Anywhere.ArcGIS.Test.Integration/GeometryGatewayTests.cs +++ b/tests/Anywhere.ArcGIS.Test.Integration/GeometryGatewayTests.cs @@ -45,14 +45,16 @@ public async Task CanProject() Assert.NotEqual(projectedFeatures[0].Geometry.Rings[0], features[0].Geometry.Rings[0]); } - [Fact] - public async Task CanBuffer() + [Theory] + [InlineData("http://sampleserver1.arcgisonline.com/ArcGIS", "Demographics/ESRI_Census_USA/MapServer/5", "STATE_NAME = 'Texas'")] + [InlineData("https://sampleserver6.arcgisonline.com/arcgis", "WorldTimeZones/MapServer/2", "REGION = 'Southeastern Asia'")] + public async Task CanBuffer(string rootUrl, string relativeUrl, string where) { - var gateway = new PortalGateway("http://sampleserver1.arcgisonline.com/ArcGIS"); + var gateway = new PortalGateway(rootUrl); var result = await IntegrationTestFixture.TestPolicy.ExecuteAsync(() => { - return gateway.Query(new Query("Demographics/ESRI_Census_USA/MapServer/5".AsEndpoint()) { Where = "STATE_NAME = 'Texas'" }); + return gateway.Query(new Query(relativeUrl) { Where = where }); }); var features = result.Features.Where(f => f.Geometry.Rings.Any()).ToList(); @@ -72,6 +74,7 @@ async Task Buffer(PortalGatewayBase gateway, List> features, Sp }); Assert.NotNull(featuresBuffered); + Assert.NotNull(featuresBuffered.FirstOrDefault()?.Geometry); Assert.Equal(featuresCount, featuresBuffered.Count); }