diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..3ff50bb --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,11 @@ +Why the change? Well there are 2 main reasons, the first is that this is now a netstandard library rather than a portable class library (PCL), so the old naming didn't really apply. Secondly, NuGet now has package Id reservations and so I can't use ArcGIS as the suffix anymore. + +To migrate from ArcGIS.PCL to Anywhere.ArcGIS the following breaking changes need to be done / reviewed: + +- All namespaces have changed from `ArcGIS.ServiceModel.*` to `Anywhere.ArcGIS.*`. + +- You no longer need to call the static `ISerializer` `Init()` method as JSON.NET is now baked in. + +- `SecurePortalGateway` has been renamed to just `PortalGateway`. + +- Internally all requests now use an `ArcGISServerOperation` type, this allows before and after actions to be invoked for the HTTP request. \ No newline at end of file diff --git a/NUGET-DOC.md b/NUGET-DOC.md new file mode 100644 index 0000000..e07aad8 --- /dev/null +++ b/NUGET-DOC.md @@ -0,0 +1,56 @@ +If you are calling a REST operation you will need to create a gateway to manage the request. There are a few different ones but the most basic is called `PortalGateway` and this can be used for connecting directly to services with ArcGIS Server. + +Create an instance of that by specifying the root url of your server. The format of the root url is _scheme_://_host_:_port_/_instance_ so a typical default ArcGIS Server for your local machine would be _http://localhost:6080/arcgis_, note that you do not need to include `rest/services` in either the root url or your relative urls as it gets added automatically. One thing to look out for is that the url is case sensitive so make sure you enter it correctly. + +```c# +var gateway = new PortalGateway("https://sampleserver3.arcgisonline.com/ArcGIS/"); +``` + +Now you have access to the various operations supported by it. For example to call a query against a service + +```c# +var query = new Query("Earthquakes/EarthquakesFromLastSevenDays/MapServer/0".AsEndpoint()) +{ + Where = "magnitude > 4.0" +}; +var result = await gateway.Query(query); +``` + +### Capabilities + +Supports the following as typed operations: + + - `CheckGenerateToken` create a token automatically via an `ITokenProvider` + - `Query` query a layer by attribute and / or spatial filters, also possible to do `BatchQuery` + - `QueryForCount` only return the number of results for the query operation + - `QueryForIds` only return the ObjectIds for the results of the query operation + - `QueryForExtent` return the bounding extent for the result of the query operation + - `Find` search across _n_ layers and fields in a service + - `ApplyEdits` post adds, updates and deletes to a feature service layer + - `Geocode` single line of input to perform a geocode using a custom locator or the Esri world locator + - `Suggest` lightweight geocode operation that only returns text results, commonly used for predictive searching + - `ReverseGeocode` find location candidates for a input point location + - `Simplify` alter geometries to be topologically consistent + - `Project` convert geometries to a different spatial reference + - `Buffer` buffers geometries by the distance requested + - `DescribeSite` returns a url for every service discovered + - `CreateReplica` create a replica for a layer + - `UnregisterReplica` unregister a replica based on the Id + - `DeleteAttachments` delete attachments that are associated with a feature + - `Ping` verify that the server can be accessed + - `Info` return the server information such as version and token authentication settings + - `DescribeServices` return services information (name, sublayers etc.) + - `DescribeService` return service information (name, sublayers etc.) + - `DescribeLayer` return layer information + +REST admin operations: + + - `PublicKey` - admin operation to get public key used for encryption of token requests + - `ServiceStatus` - admin operation to get the configured and actual status of a service + - `ServiceReport` - admin operation to get the service report + - `StartService` - admin operation to start a service + - `StopService` - admin operation to stop a service + +There are also methods to add / update and download attachments for a feature and you can extend this library by writing your own operations. + +Refer to the integration test project for more examples.s \ No newline at end of file diff --git a/README.md b/README.md index bb74667..b39026f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Use ArcGIS Server REST resources without an official SDK. Ths is a netstandard 2 A typical use case would be the need to call some ArcGIS REST resource from server .NET code or maybe a console app. The features that this returns can be used directly as Esri JSON in JavaScript apps using the Esri JS API. -Works with secure and non-secure ArcGIS Server on premise / in the cloud, Portal for ArcGIS and ArcGIS Online. +Works with secure and non-secure ArcGIS Server on premise / in the cloud, Portal for ArcGIS and ArcGIS Online. Also supports converting GeoJSON <-> ArcGIS Features. ### Quickstart @@ -73,7 +73,7 @@ Refer to the integration test project for more examples. Absolutely! Please feel free to raise issues, fork the source code, send pull requests, etc. -No pull request is too small. Even whitespace fixes are appreciated. Before you contribute anything make sure you read [CONTRIBUTING](https://github.com/davetimmins/Anywhere.ArcGIS/CONTRIBUTING.md). +No pull request is too small. Even whitespace fixes are appreciated. Before you contribute anything make sure you read [CONTRIBUTING](https://github.com/davetimmins/Anywhere.ArcGIS/blob/master/CONTRIBUTING.md). ### Installation @@ -97,5 +97,5 @@ Anywhere.ArcGIS uses [Semantic Versioning](http://semver.org/). ### Icon -Icon made by [Freepik](http://www.freepik.com) from [www.flaticon.com](http://www.flaticon.com/free-icon/triangle-of-triangles_32915) +Icon made by [Freepik](http://www.freepik.com) from [www.flaticon.com](https://www.flaticon.com/free-icon/triangle-of-triangles_32915) diff --git a/src/Anywhere.ArcGIS/Anywhere.ArcGIS.csproj b/src/Anywhere.ArcGIS/Anywhere.ArcGIS.csproj index c7e112d..cb672bf 100644 --- a/src/Anywhere.ArcGIS/Anywhere.ArcGIS.csproj +++ b/src/Anywhere.ArcGIS/Anywhere.ArcGIS.csproj @@ -14,7 +14,7 @@ git ArcGIS ArcGISServer ArcGISOnline Esri REST netstandard anywhere GIS Mapping Map Location GeoLocation OAuth Initial port of ArcGIS.PCL to netstandard - 1.0.0-beta.1 + 1.0.0-beta.2 diff --git a/src/Anywhere.ArcGIS/Common/Feature.cs b/src/Anywhere.ArcGIS/Common/Feature.cs index 81ae1df..38cfd9d 100644 --- a/src/Anywhere.ArcGIS/Common/Feature.cs +++ b/src/Anywhere.ArcGIS/Common/Feature.cs @@ -11,7 +11,7 @@ namespace Anywhere.ArcGIS.Common /// Type of geometry that the feature represents. /// All properties are optional. [DataContract] - public class Feature : IEquatable> where T : IGeometry + public class Feature : IEquatable> where T : IGeometry { const string ObjectIDName = "objectid"; const string GlobalIDName = "globalid"; diff --git a/src/Anywhere.ArcGIS/Common/IGeometry.cs b/src/Anywhere.ArcGIS/Common/IGeometry.cs index 5429ac7..c3cd39a 100644 --- a/src/Anywhere.ArcGIS/Common/IGeometry.cs +++ b/src/Anywhere.ArcGIS/Common/IGeometry.cs @@ -1,4 +1,5 @@ -using System; +using Anywhere.ArcGIS.GeoJson; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; @@ -14,10 +15,10 @@ namespace Anywhere.ArcGIS.Common /// Envelopes /// /// Starting at ArcGIS Server 10.1, geometries containing m and z values are supported - public interface IGeometry + public interface IGeometry { /// - /// The spatial reference can be defined using a well-known ID (wkid) or well-known text (wkt) + /// The spatial reference can be defined using a well-known ID (wkid) or well-known text (wkt) /// [DataMember(Name = "spatialReference")] SpatialReference SpatialReference { get; set; } @@ -35,10 +36,10 @@ public interface IGeometry Point GetCenter(); /// - /// Get the associated geometry + /// Converts the geometry to its GeoJSON representation /// - /// The geometry object - T GetGeometry(); + /// The corresponding GeoJSON for the geometry + IGeoJsonGeometry ToGeoJson(); } /// @@ -138,7 +139,7 @@ public override int GetHashCode() } [DataContract] - public class Point : IGeometry, IEquatable + public class Point : IGeometry, IEquatable { [DataMember(Name = "spatialReference", Order = 5)] public SpatialReference SpatialReference { get; set; } @@ -165,12 +166,6 @@ public Point GetCenter() return new Point { X = X, Y = Y, SpatialReference = SpatialReference }; } - public Point GetGeometry() - { - // TODO : make immutable? - return this; - } - public bool Equals(Point other) { if (ReferenceEquals(null, other)) return false; @@ -199,14 +194,14 @@ public override bool Equals(object obj) return Equals((Point)obj); } - //public IGeoJsonGeometry ToGeoJson() - //{ - // return new GeoJsonPoint { Type = "Point", Coordinates = new[] { X, Y } }; - //} + public IGeoJsonGeometry ToGeoJson() + { + return new GeoJsonPoint { Type = "Point", Coordinates = new[] { X, Y } }; + } } [DataContract] - public class MultiPoint : IGeometry, IEquatable + public class MultiPoint : IGeometry, IEquatable { [DataMember(Name = "spatialReference", Order = 4)] public SpatialReference SpatialReference { get; set; } @@ -230,11 +225,6 @@ public Point GetCenter() return GetExtent().GetCenter(); } - public MultiPoint GetGeometry() - { - return this; - } - public bool Equals(MultiPoint other) { if (ReferenceEquals(null, other)) return false; @@ -262,14 +252,14 @@ public override bool Equals(object obj) return Equals((MultiPoint)obj); } - //public IGeoJsonGeometry ToGeoJson() - //{ - // return new GeoJsonLineString { Type = "MultiPoint", Coordinates = Points }; - //} + public IGeoJsonGeometry ToGeoJson() + { + return new GeoJsonLineString { Type = "MultiPoint", Coordinates = Points }; + } } [DataContract] - public class Polyline : IGeometry, IEquatable + public class Polyline : IGeometry, IEquatable { [DataMember(Name = "spatialReference", Order = 4)] public SpatialReference SpatialReference { get; set; } @@ -303,11 +293,6 @@ public Point GetCenter() return GetExtent().GetCenter(); } - public Polyline GetGeometry() - { - return this; - } - public bool Equals(Polyline other) { if (ReferenceEquals(null, other)) return false; @@ -335,10 +320,10 @@ public override bool Equals(object obj) return Equals((Polyline)obj); } - //public IGeoJsonGeometry ToGeoJson() - //{ - // return Paths.Any() ? new GeoJsonLineString { Type = "LineString", Coordinates = Paths.First() } : null; - //} + public IGeoJsonGeometry ToGeoJson() + { + return Paths.Any() ? new GeoJsonLineString { Type = "LineString", Coordinates = Paths.First() } : null; + } } public class PointCollection : List @@ -377,7 +362,7 @@ public class PointCollectionList : List { } [DataContract] - public class Polygon : IGeometry, IEquatable + public class Polygon : IGeometry, IEquatable { [DataMember(Name = "spatialReference", Order = 4)] public SpatialReference SpatialReference { get; set; } @@ -411,11 +396,6 @@ public Point GetCenter() return GetExtent().GetCenter(); } - public Polygon GetGeometry() - { - return this; - } - public bool Equals(Polygon other) { if (ReferenceEquals(null, other)) return false; @@ -443,14 +423,14 @@ public override bool Equals(object obj) return Equals((Polygon)obj); } - //public IGeoJsonGeometry ToGeoJson() - //{ - // return new GeoJsonPolygon { Type = "Polygon", Coordinates = Rings }; - //} + public IGeoJsonGeometry ToGeoJson() + { + return new GeoJsonPolygon { Type = "Polygon", Coordinates = Rings }; + } } [DataContract] - public class Extent : IGeometry, IEquatable + public class Extent : IGeometry, IEquatable { [DataMember(Name = "spatialReference", Order = 5)] public SpatialReference SpatialReference { get; set; } @@ -477,11 +457,6 @@ public Point GetCenter() return new Point { X = ((XMin + XMax) / 2), Y = ((YMin + YMax) / 2), SpatialReference = SpatialReference }; } - public Extent GetGeometry() - { - return this; - } - public Extent Union(Extent extent) { if (extent == null) extent = this; @@ -548,23 +523,23 @@ public override bool Equals(object obj) return Equals((Extent)obj); } - //public IGeoJsonGeometry ToGeoJson() - //{ - // return new GeoJsonPolygon - // { - // Type = "Polygon", - // Coordinates = new PointCollectionList - // { - // new PointCollection - // { - // new[]{ XMin, YMin }, - // new[]{ XMax, YMin }, - // new[]{ XMax, YMax }, - // new[]{ XMin, YMax }, - // new[]{ XMin, YMin } - // } - // } - // }; - //} + public IGeoJsonGeometry ToGeoJson() + { + return new GeoJsonPolygon + { + Type = "Polygon", + Coordinates = new PointCollectionList + { + new PointCollection + { + new[]{ XMin, YMin }, + new[]{ XMax, YMin }, + new[]{ XMax, YMax }, + new[]{ XMin, YMax }, + new[]{ XMin, YMin } + } + } + }; + } } } diff --git a/src/Anywhere.ArcGIS/Extensions/FeatureCollectionExtensions.cs b/src/Anywhere.ArcGIS/Extensions/FeatureCollectionExtensions.cs index e367e9f..3eee705 100644 --- a/src/Anywhere.ArcGIS/Extensions/FeatureCollectionExtensions.cs +++ b/src/Anywhere.ArcGIS/Extensions/FeatureCollectionExtensions.cs @@ -1,4 +1,6 @@ using Anywhere.ArcGIS.Common; +using Anywhere.ArcGIS.GeoJson; +using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +11,72 @@ namespace Anywhere.ArcGIS /// public static class FeatureCollectionExtensions { + readonly static Dictionary> _typeMap = new Dictionary> + { + { "Point", () => typeof(Point) }, + { "MultiPoint", () => typeof(MultiPoint) }, + { "LineString", () => typeof(Polyline) }, + { "MultiLineString", () => typeof(Polyline) }, + { "Polygon", () => typeof(Polygon) }, + { "MultiPolygon", () => typeof(Polygon) } + }; + + /// + /// Convert a GeoJSON FeatureCollection into an ArcGIS FeatureSet + /// + /// The type of GeoJSON geometry to convert. Can be Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon + /// A collection of one or more features of the same geometry type + /// A converted set of features that can be used in ArcGIS Server operations + public static List> ToFeatures(this FeatureCollection featureCollection) + where TGeometry : IGeoJsonGeometry + { + if (featureCollection == null || featureCollection.Features == null || !featureCollection.Features.Any()) return null; + + var features = new List>(); + + foreach (var geoJson in featureCollection.Features) + { + var geometry = geoJson.Geometry.ToGeometry(_typeMap[geoJson.Geometry.Type]()); + if (geometry == null) continue; + + features.Add(new Feature { Geometry = geometry, Attributes = geoJson.Properties }); + } + return features; + } + + /// + /// Convert an ArcGIS Feature Set into a GeoJSON FeatureCollection + /// + /// The type of ArcGIS geometry to convert. + /// A collection of one or more ArcGIS Features + /// A converted FeatureCollection of GeoJSON Features + public static FeatureCollection ToFeatureCollection(this List> features) + where TGeometry : IGeometry + { + if (features == null || !features.Any()) return null; + + var featureCollection = new FeatureCollection { Features = new List>() }; + if (features.First().Geometry.SpatialReference != null) + featureCollection.CoordinateReferenceSystem = new Crs + { + Type = "EPSG", + Properties = new CrsProperties { Wkid = (int)features.First().Geometry.SpatialReference.Wkid } + }; + + foreach (var feature in features) + { + var geoJsonGeometry = feature.Geometry.ToGeoJson(); + if (geoJsonGeometry == null) continue; + featureCollection.Features.Add(new GeoJsonFeature + { + Type = "Feature", + Geometry = geoJsonGeometry, + Properties = feature.Attributes + }); + } + return featureCollection; + } + /// /// Updates the geometries of the feature collection but preserves the attributes and order of features /// @@ -16,7 +84,7 @@ public static class FeatureCollectionExtensions /// collection of features to update /// The updated geometries /// An updated collection of features in the same order as was passed in - public static List> UpdateGeometries(this List> features, List geometries) where T : IGeometry + public static List> UpdateGeometries(this List> features, List geometries) where T : IGeometry { var result = new List>(); @@ -24,18 +92,11 @@ public static List> UpdateGeometries(this List> feature { var attr = i < features.Count ? features[i].Attributes : null; var feature = new Feature { Attributes = attr }; - if (i < geometries.Count) - { - feature.Geometry = geometries[i]; - } + if (i < geometries.Count) feature.Geometry = geometries[i]; result.Insert(i, feature); } if (geometries.Count > features.Count) - { - result - .InsertRange(features.Count, geometries.Skip(features.Count) - .Select(g => new Feature { Geometry = g })); - } + result.InsertRange(features.Count, geometries.Skip(features.Count).Select(g => new Feature { Geometry = g })); return result; } diff --git a/src/Anywhere.ArcGIS/GeoJson/FeatureCollection.cs b/src/Anywhere.ArcGIS/GeoJson/FeatureCollection.cs new file mode 100644 index 0000000..2ce9957 --- /dev/null +++ b/src/Anywhere.ArcGIS/GeoJson/FeatureCollection.cs @@ -0,0 +1,173 @@ +using Anywhere.ArcGIS.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace Anywhere.ArcGIS.GeoJson +{ + [DataContract] + public class FeatureCollection where TGeometry : IGeoJsonGeometry + { + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "bbox")] + public double[] BoundingBox { get; set; } + + [DataMember(Name = "features")] + public List> Features { get; set; } + + [DataMember(Name = "crs")] + public Crs CoordinateReferenceSystem { get; set; } + } + + [DataContract] + public class Crs + { + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "properties")] + public CrsProperties Properties { get; set; } + } + + [DataContract] + public class CrsProperties + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "href")] + public string Href { get; set; } + + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "code")] + public int Wkid { get; set; } + } + + [DataContract] + public class GeoJsonFeature where TGeometry : IGeoJsonGeometry + { + [DataMember(Name = "id")] + public object Id { get; set; } + + [DataMember(Name = "properties")] + public Dictionary Properties { get; set; } + + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "bbox")] + public double[] BoundingBox { get; set; } + + [DataMember(Name = "geometry")] + public TGeometry Geometry { get; set; } + } + + [DataContract] + public class GeoJsonPoint : IGeoJsonGeometry + { + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "coordinates")] + public double[] Coordinates { get; set; } + + public IGeometry ToGeometry(Type type) + { + if (Coordinates == null || Coordinates.Count() != 2) + { + return null; + } + + return new Point { X = Coordinates.First(), Y = Coordinates.Last() }; + } + } + + [DataContract] + public class GeoJsonLineString : IGeoJsonGeometry + { + readonly static Dictionary> _geoJsonFactoryMap = new Dictionary> + { + { typeof(MultiPoint), (coords) => new MultiPoint { Points = coords } }, + { typeof(Polyline), (coords) => new Polyline { Paths = new PointCollectionList { coords } } } + }; + + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "coordinates")] + public PointCollection Coordinates { get; set; } + + public IGeometry ToGeometry(Type type) + { + if (Coordinates == null) + { + return null; + } + + return _geoJsonFactoryMap[type](Coordinates); + } + } + + [DataContract] + public class GeoJsonPolygon : IGeoJsonGeometry + { + static Dictionary> _geoJsonFactoryMap = new Dictionary> + { + { typeof(Polygon), (coords) => new Polygon { Rings = coords } }, + { typeof(Polyline), (coords) => new Polyline { Paths = coords } } + }; + + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "coordinates")] + public PointCollectionList Coordinates { get; set; } + + public IGeometry ToGeometry(Type type) + { + if (Coordinates == null) + { + return null; + } + + return _geoJsonFactoryMap[type](Coordinates); + } + } + + [DataContract] + public class GeoJsonMultiPolygon : IGeoJsonGeometry + { + [DataMember(Name = "type")] + public string Type { get; set; } + + [DataMember(Name = "coordinates")] + public List Coordinates { get; set; } + + public IGeometry ToGeometry(Type type) + { + if (Coordinates == null) return null; + + var poly = new Polygon { Rings = new PointCollectionList() }; + + foreach (var polygon in Coordinates) + { + poly.Rings.AddRange(polygon); + } + + return poly; + } + } + + public interface IGeoJsonGeometry + { + [DataMember(Name = "type")] + string Type { get; set; } + + IGeometry ToGeometry(Type type); + } +} diff --git a/src/Anywhere.ArcGIS/Operation/ApplyEdits.cs b/src/Anywhere.ArcGIS/Operation/ApplyEdits.cs index 2e19f90..046a121 100644 --- a/src/Anywhere.ArcGIS/Operation/ApplyEdits.cs +++ b/src/Anywhere.ArcGIS/Operation/ApplyEdits.cs @@ -7,11 +7,12 @@ namespace Anywhere.ArcGIS.Operation { /// - /// This operation adds, updates and deletes features to the associated feature layer or table in a single call (POST only). + /// This operation adds, updates and deletes features to the associated feature layer or table in a single call (POST only). /// /// [DataContract] - public class ApplyEdits : ArcGISServerOperation where T : IGeometry + public class ApplyEdits : ArcGISServerOperation + where T : IGeometry { public ApplyEdits(ArcGISServerEndpoint endpoint, Action beforeRequest = null, Action afterRequest = null) : base(endpoint.RelativeUrl.Trim('/') + "/" + Operations.ApplyEdits, beforeRequest, afterRequest) @@ -20,7 +21,7 @@ public ApplyEdits(ArcGISServerEndpoint endpoint, Action beforeRequest = null, Ac Updates = new List>(); Deletes = new List(); } - + /// /// The array of features to be added. /// @@ -110,7 +111,8 @@ IEnumerable CheckResults(List results, boo return results.Where(r => r.Success == success); } - public void SetExpected(ApplyEdits operation) where T : IGeometry + public void SetExpected(ApplyEdits operation) + where T : IGeometry { if (operation == null) return; diff --git a/src/Anywhere.ArcGIS/Operation/CreateReplica.cs b/src/Anywhere.ArcGIS/Operation/CreateReplica.cs index e452ae8..466f3f1 100644 --- a/src/Anywhere.ArcGIS/Operation/CreateReplica.cs +++ b/src/Anywhere.ArcGIS/Operation/CreateReplica.cs @@ -16,7 +16,7 @@ /// It requires the Sync capability. /// [DataContract] - public class CreateReplica : ArcGISServerOperation where T : IGeometry + public class CreateReplica : ArcGISServerOperation { public const string Operation = "createReplica"; @@ -54,11 +54,11 @@ public string GeometryType public SpatialReference InputGeometrySpatialReference { get; set; } /// - /// (Required) The geometry to apply as the spatial filter. + /// (Required) The geometry to apply as the spatial filter. /// All the features in layers intersecting this geometry will be replicated /// [DataMember(Name = "geometry")] - public IGeometry Geometry { get; set; } + public IGeometry Geometry { get; set; } internal static readonly Dictionary> TypeMap = new Dictionary> { @@ -67,8 +67,8 @@ public string GeometryType { typeof(Extent), () => GeometryTypes.Envelope }, { typeof(Polygon), () => GeometryTypes.Polygon }, { typeof(Polyline), () => GeometryTypes.Polyline } - }; - + }; + /// /// Gets or sets the name of the replica on the server. The replica name is unique per feature service. /// This is not a required parameter. @@ -97,8 +97,8 @@ public Dictionary LayerQueriesValue { get { return LayerQueries == null || !LayerQueries.Any() ? null : LayerQueries.ToDictionary(k => k.Id, v => v); } } /// - /// Specifies whether the replica is to be used on a client, such as a mobile device or ArcGIS Pro, or on another server. - /// Specifying server allows you to publish the replica to another portal and then synchronize changes between two feature services. + /// Specifies whether the replica is to be used on a client, such as a mobile device or ArcGIS Pro, or on another server. + /// Specifying server allows you to publish the replica to another portal and then synchronize changes between two feature services. /// The default is client. /// [DataMember(Name = "targetType")] @@ -171,7 +171,7 @@ public bool ReturnAttachments /// [DataMember(Name = "attachmentsSyncDirection")] public string AttachmentsSyncDirection { get; set; } - + /// /// The format of the replica geodatabase returned in the response. /// sqlite or json are valid values. @@ -214,17 +214,17 @@ public LayerQuery() public string Where { get; set; } /// - /// Determines whether or not to apply the geometry for the layer. + /// Determines whether or not to apply the geometry for the layer. /// The default is true. If set to false, features from the layer that intersect the geometry are not added. /// [DataMember(Name = "useGeometry")] public bool UseGeometry { get; set; } /// - /// Determines whether or not to add related rows. - /// The default is true. - /// Value true is honored only for queryOption = none. - /// This is only applicable if your data has relationship classes. + /// Determines whether or not to add related rows. + /// The default is true. + /// Value true is honored only for queryOption = none. + /// This is only applicable if your data has relationship classes. /// Relationships are only processed in a forward direction from origin to destination. /// [DataMember(Name = "includeRelated")] @@ -244,7 +244,7 @@ public UnregisterReplica(ArcGISServerEndpoint endpoint, Action beforeRequest = n [DataContract] public class ArcGISReplica : PortalResponse - where T : IGeometry + where T : IGeometry { [DataMember(Name = "replicaName")] public string Name { get; set; } @@ -276,7 +276,7 @@ public class ArcGISReplica : PortalResponse [DataContract] public class ReplicaLayer - where T : IGeometry + where T : IGeometry { [DataMember(Name = "id")] public int Id { get; set; } diff --git a/src/Anywhere.ArcGIS/Operation/GeometryServerOperation.cs b/src/Anywhere.ArcGIS/Operation/GeometryServerOperation.cs index c0ebb71..302ae29 100644 --- a/src/Anywhere.ArcGIS/Operation/GeometryServerOperation.cs +++ b/src/Anywhere.ArcGIS/Operation/GeometryServerOperation.cs @@ -7,24 +7,26 @@ namespace Anywhere.ArcGIS.Operation { [DataContract] - public class GeometryOperationResponse : PortalResponse where T : IGeometry + public class GeometryOperationResponse : PortalResponse + where T : IGeometry { [DataMember(Name = "geometries")] public List Geometries { get; set; } } [DataContract] - public class SimplifyGeometry : ArcGISServerOperation where T : IGeometry + public class SimplifyGeometry : ArcGISServerOperation + where T : IGeometry { public SimplifyGeometry(IEndpoint endpoint, List> features = null, SpatialReference spatialReference = null, Action beforeRequest = null, Action afterRequest = null) - : base((endpoint is AbsoluteEndpoint) + : base((endpoint is AbsoluteEndpoint) ? (IEndpoint)new AbsoluteEndpoint(endpoint.RelativeUrl.Trim('/') + "/" + Operations.Simplify) : (IEndpoint)new ArcGISServerEndpoint(endpoint.RelativeUrl.Trim('/') + "/" + Operations.Simplify), beforeRequest, afterRequest) - { + { Geometries = new GeometryCollection { Geometries = features?.Select(f => f.Geometry).ToList() }; SpatialReference = spatialReference; - } + } [DataMember(Name = "geometries")] public GeometryCollection Geometries { get; set; } @@ -34,7 +36,8 @@ public SimplifyGeometry(IEndpoint endpoint, List> features = null, Sp } [DataContract] - public class BufferGeometry : GeometryOperation where T : IGeometry + public class BufferGeometry : GeometryOperation + where T : IGeometry { public BufferGeometry(IEndpoint endpoint, List> features, SpatialReference spatialReference, double distance) : base(endpoint, features, spatialReference, Operations.Buffer) @@ -77,15 +80,15 @@ public string DistancesCSV public string Unit { get; set; } /// - /// If true, all geometries buffered at a given distance are unioned into a single (possibly multipart) polygon, - /// and the unioned geometry is placed in the output array. + /// If true, all geometries buffered at a given distance are unioned into a single (possibly multipart) polygon, + /// and the unioned geometry is placed in the output array. /// The default is false. /// [DataMember(Name = "unionResults")] public bool UnionResults { get; set; } /// - /// Set geodesic to true to buffer the input geometries using geodesic distance. Geodesic distance is the shortest path between two points along the ellipsoid of the earth. + /// Set geodesic to true to buffer the input geometries using geodesic distance. Geodesic distance is the shortest path between two points along the ellipsoid of the earth. /// If geodesic is set to false, the 2D Euclidean distance is used to buffer the input geometries. /// [DataMember(Name = "geodesic")] @@ -93,24 +96,25 @@ public string DistancesCSV } [DataContract] - public class ProjectGeometry : GeometryOperation where T : IGeometry + public class ProjectGeometry : GeometryOperation + where T : IGeometry { public ProjectGeometry(IEndpoint endpoint, List> features, SpatialReference outputSpatialReference) : base(endpoint, features, outputSpatialReference, Operations.Project) { } /// - /// The WKID or a JSON object specifying the geographic transformation (also known as datum transformation) to be applied to the - /// projected geometries. - /// Note that a transformation is needed only if the output spatial reference contains a different geographic coordinate system + /// The WKID or a JSON object specifying the geographic transformation (also known as datum transformation) to be applied to the + /// projected geometries. + /// Note that a transformation is needed only if the output spatial reference contains a different geographic coordinate system /// than the input spatial reference. /// [DataMember(Name = "transformation")] public string Transformation { get; set; } /// - /// A Boolean value indicating whether or not to transform forward. - /// The forward or reverse direction of transformation is implied in the name of the transformation. + /// A Boolean value indicating whether or not to transform forward. + /// The forward or reverse direction of transformation is implied in the name of the transformation. /// If Transformation is specified, a value for the TransformForward parameter must also be specified. The default value is false. /// /// @@ -121,7 +125,8 @@ public ProjectGeometry(IEndpoint endpoint, List> features, SpatialRef } [DataContract] - public class GeometryCollection where T : IGeometry + public class GeometryCollection + where T : IGeometry { [DataMember(Name = "geometryType")] public string GeometryType @@ -138,18 +143,19 @@ public string GeometryType public List Geometries { get; set; } } - public abstract class GeometryOperation : ArcGISServerOperation where T : IGeometry + public abstract class GeometryOperation : ArcGISServerOperation + where T : IGeometry { public GeometryOperation(IEndpoint endpoint, List> features, SpatialReference outputSpatialReference, string operation, Action beforeRequest = null, Action afterRequest = null) - : base((endpoint is AbsoluteEndpoint) + : base((endpoint is AbsoluteEndpoint) ? (IEndpoint)new AbsoluteEndpoint(endpoint.RelativeUrl.Trim('/') + "/" + operation) : (IEndpoint)new ArcGISServerEndpoint(endpoint.RelativeUrl.Trim('/') + "/" + operation), beforeRequest, afterRequest) - { + { Features = features; if (features.Any()) { @@ -160,7 +166,7 @@ public GeometryOperation(IEndpoint endpoint, } OutputSpatialReference = outputSpatialReference; } - + [IgnoreDataMember] public List> Features { get; private set; } @@ -174,7 +180,7 @@ public GeometryOperation(IEndpoint endpoint, public SpatialReference InputSpatialReference { get { return Geometries.Geometries.First().SpatialReference ?? SpatialReference.WGS84; } } /// - /// The spatial reference of the returned geometry. + /// The spatial reference of the returned geometry. /// If not specified, the geometry is returned in the spatial reference of the input. /// [DataMember(Name = "outSR")] diff --git a/src/Anywhere.ArcGIS/Operation/Query.cs b/src/Anywhere.ArcGIS/Operation/Query.cs index 423e01a..bb35b47 100644 --- a/src/Anywhere.ArcGIS/Operation/Query.cs +++ b/src/Anywhere.ArcGIS/Operation/Query.cs @@ -6,12 +6,31 @@ namespace Anywhere.ArcGIS.Operation { + /// + /// Basic query request operation + /// [DataContract] - public class Query : Query where T : IGeometry + public class Query : ArcGISServerOperation { + /// + /// Represents a request for a query against a service resource + /// + /// Resource to apply the query against public Query(ArcGISServerEndpoint endpoint, Action beforeRequest = null, Action afterRequest = null) - : base(endpoint, beforeRequest, afterRequest) - { } + : base(endpoint.RelativeUrl.Trim('/') + "/" + Operations.Query, beforeRequest, afterRequest) + { + Where = "1=1"; + OutFields = new List(); + ReturnGeometry = true; + SpatialRelationship = SpatialRelationshipTypes.Intersects; + } + + /// + /// A where clause for the query filter. Any legal SQL where clause operating on the fields in the layer is allowed. + /// + /// Default is '1=1' + [DataMember(Name = "where")] + public string Where { get; set; } /// /// The geometry to apply as the spatial filter. @@ -19,7 +38,7 @@ public Query(ArcGISServerEndpoint endpoint, Action beforeRequest = null, Action /// /// Default is empty [DataMember(Name = "geometry")] - public IGeometry Geometry { get; set; } + public IGeometry Geometry { get; set; } /// /// The spatial reference of the input geometry. @@ -47,34 +66,6 @@ public string GeometryType } } - } - - /// - /// Basic query request operation - /// - [DataContract] - public class Query : ArcGISServerOperation - { - /// - /// Represents a request for a query against a service resource - /// - /// Resource to apply the query against - public Query(ArcGISServerEndpoint endpoint, Action beforeRequest = null, Action afterRequest = null) - : base(endpoint.RelativeUrl.Trim('/') + "/" + Operations.Query, beforeRequest, afterRequest) - { - Where = "1=1"; - OutFields = new List(); - ReturnGeometry = true; - SpatialRelationship = SpatialRelationshipTypes.Intersects; - } - - /// - /// A where clause for the query filter. Any legal SQL where clause operating on the fields in the layer is allowed. - /// - /// Default is '1=1' - [DataMember(Name = "where")] - public string Where { get; set; } - /// /// The names of the fields to search. /// @@ -100,7 +91,7 @@ public Query(ArcGISServerEndpoint endpoint, Action beforeRequest = null, Action /// [DataMember(Name = "objectIds")] public string ObjectIdsValue { get { return ObjectIds == null || !ObjectIds.Any() ? null : string.Join(",", ObjectIds); } } - + /// /// The spatial reference of the returned geometry. @@ -108,8 +99,8 @@ public Query(ArcGISServerEndpoint endpoint, Action beforeRequest = null, Action /// [DataMember(Name = "outSR")] public SpatialReference OutputSpatialReference { get; set; } - - + + /// /// The spatial relationship to be applied on the input geometry while performing the query. @@ -227,7 +218,8 @@ public string Time } [DataContract] - public class QueryResponse : PortalResponse where T : IGeometry + public class QueryResponse : PortalResponse + where T : IGeometry { public QueryResponse() { diff --git a/src/Anywhere.ArcGIS/PortalGatewayBase.cs b/src/Anywhere.ArcGIS/PortalGatewayBase.cs index b01f45f..1daaea9 100644 --- a/src/Anywhere.ArcGIS/PortalGatewayBase.cs +++ b/src/Anywhere.ArcGIS/PortalGatewayBase.cs @@ -138,58 +138,16 @@ public TimeSpan HttpRequestTimeout /// Query filter parameters /// Optional cancellation token to cancel pending request /// The matching features for the query - public virtual Task> Query(Query queryOptions, CancellationToken ct = default(CancellationToken)) where Tout : IGeometry where Tin : IGeometry + public virtual Task> Query(Query queryOptions, CancellationToken ct = default(CancellationToken)) + where T : IGeometry { - return Get, Query>(queryOptions, ct); + return Get, Query>(queryOptions, ct); } - public virtual Task> Query(Query queryOptions, CancellationToken ct = default(CancellationToken)) where Tout : IGeometry + public virtual async Task> BatchQuery(Query queryOptions, CancellationToken ct = default(CancellationToken)) + where T : IGeometry { - return Get, Query>(queryOptions, ct); - } - - public virtual async Task> BatchQuery(Query queryOptions, CancellationToken ct = default(CancellationToken)) - where Tout : IGeometry - where Tin : IGeometry - { - var result = await Get, Query>(queryOptions, ct); - - if (result != null && result.Error == null && result.Features != null && result.Features.Any() && result.ExceededTransferLimit.HasValue && result.ExceededTransferLimit.Value == true) - { - // need to get the remaining data since we went over the limit - var batchSize = result.Features.Count(); - var loop = 1; - var exceeded = true; - - while (exceeded == true) - { - _logger.InfoFormat("Exceeded query transfer limit (found {0}), batching query for {1} - loop {2}", batchSize, queryOptions.RelativeUrl, loop); - var innerQueryOptions = queryOptions; - innerQueryOptions.ResultOffset = batchSize * loop; - innerQueryOptions.ResultRecordCount = batchSize; - var innerResult = await Get, Query>(queryOptions, ct).ConfigureAwait(false); - - if (innerResult != null && innerResult.Error == null && innerResult.Features != null && innerResult.Features.Any()) - { - result.Features.ToList().AddRange(innerResult.Features); - exceeded = result.ExceededTransferLimit.HasValue && innerResult.ExceededTransferLimit.Value; - } - else - { - exceeded = false; - } - - loop++; - } - } - - return result; - } - - public virtual async Task> BatchQuery(Query queryOptions, CancellationToken ct = default(CancellationToken)) - where Tout : IGeometry - { - var result = await Get, Query>(queryOptions, ct); + var result = await Get, Query>(queryOptions, ct); if (result != null && result.Error == null && result.Features != null && result.Features.Any() && result.ExceededTransferLimit.HasValue && result.ExceededTransferLimit.Value == true) { @@ -204,7 +162,7 @@ public TimeSpan HttpRequestTimeout var innerQueryOptions = queryOptions; innerQueryOptions.ResultOffset = batchSize * loop; innerQueryOptions.ResultRecordCount = batchSize; - var innerResult = await Get, Query>(queryOptions, ct).ConfigureAwait(false); + var innerResult = await Get, Query>(queryOptions, ct).ConfigureAwait(false); if (innerResult != null && innerResult.Error == null && innerResult.Features != null && innerResult.Features.Any()) { @@ -263,7 +221,8 @@ public TimeSpan HttpRequestTimeout /// The edits to perform /// Optional cancellation token to cancel pending request /// A collection of add, update and delete results - public virtual async Task ApplyEdits(ApplyEdits edits, CancellationToken ct = default(CancellationToken)) where T : IGeometry + public virtual async Task ApplyEdits(ApplyEdits edits, CancellationToken ct = default(CancellationToken)) + where T : IGeometry { var result = await Post>(edits, ct); result.SetExpected(edits); @@ -278,7 +237,8 @@ public TimeSpan HttpRequestTimeout /// The spatial reference you want the result set to be /// Optional cancellation token to cancel pending request /// The corresponding features with the newly projected geometries - public virtual async Task>> Project(List> features, SpatialReference outputSpatialReference, CancellationToken ct = default(CancellationToken)) where T : IGeometry + public virtual async Task>> Project(List> features, SpatialReference outputSpatialReference, CancellationToken ct = default(CancellationToken)) + where T : IGeometry { var op = new ProjectGeometry(GeometryServiceEndpoint, features, outputSpatialReference); var projected = await Post, ProjectGeometry>(op, ct).ConfigureAwait(false); @@ -290,7 +250,8 @@ public TimeSpan HttpRequestTimeout return result; } - public virtual async Task>> Project(ProjectGeometry operation, CancellationToken ct = default(CancellationToken)) where T : IGeometry + public virtual async Task>> Project(ProjectGeometry operation, CancellationToken ct = default(CancellationToken)) + where T : IGeometry { var projected = await Post, ProjectGeometry>(operation, ct).ConfigureAwait(false); @@ -310,7 +271,8 @@ public TimeSpan HttpRequestTimeout /// Distance in meters to buffer the geometries by /// Optional cancellation token to cancel pending request /// The corresponding features with the newly buffered geometries - public virtual async Task>> Buffer(List> features, SpatialReference spatialReference, double distance, CancellationToken ct = default(CancellationToken)) where T : IGeometry + public virtual async Task>> Buffer(List> features, SpatialReference spatialReference, double distance, CancellationToken ct = default(CancellationToken)) + where T : IGeometry { var op = new BufferGeometry(GeometryServiceEndpoint, features, spatialReference, distance); var buffered = await Post, BufferGeometry>(op, ct).ConfigureAwait(false); @@ -330,7 +292,8 @@ public TimeSpan HttpRequestTimeout /// The spatial reference of the geometries /// Optional cancellation token to cancel pending request /// The corresponding features with the newly simplified geometries - public virtual async Task>> Simplify(List> features, SpatialReference spatialReference, CancellationToken ct = default(CancellationToken)) where T : IGeometry + public virtual async Task>> Simplify(List> features, SpatialReference spatialReference, CancellationToken ct = default(CancellationToken)) + where T : IGeometry { var op = new SimplifyGeometry(GeometryServiceEndpoint, features, spatialReference); var simplified = await Post, SimplifyGeometry>(op, ct).ConfigureAwait(false); @@ -342,11 +305,10 @@ public TimeSpan HttpRequestTimeout return result; } - public Task> CreateReplica(CreateReplica createReplica, CancellationToken ct = default(CancellationToken)) - where Tout : IGeometry - where Tin : IGeometry + public Task> CreateReplica(CreateReplica createReplica, CancellationToken ct = default(CancellationToken)) + where T : IGeometry { - return Post, CreateReplica>(createReplica, ct); + return Post, CreateReplica>(createReplica, ct); } public Task UnregisterReplica(UnregisterReplica unregisterReplica, CancellationToken ct = default(CancellationToken)) diff --git a/tests/Anywhere.ArcGIS.Test.Integration/ArcGISGatewayTests.cs b/tests/Anywhere.ArcGIS.Test.Integration/ArcGISGatewayTests.cs index 79f9966..a8d4a9b 100644 --- a/tests/Anywhere.ArcGIS.Test.Integration/ArcGISGatewayTests.cs +++ b/tests/Anywhere.ArcGIS.Test.Integration/ArcGISGatewayTests.cs @@ -24,7 +24,7 @@ public ArcGISGateway(string root, ITokenProvider tokenProvider) : base(root, tokenProvider: tokenProvider) { } - public Task> QueryAsPost(Query queryOptions) where Tout : IGeometry + public Task> QueryAsPost(Query queryOptions) where Tout : IGeometry { return Post, Query>(queryOptions, CancellationToken.None); } @@ -512,14 +512,15 @@ async Task QueryGeometryCriteriaHonored(string serviceUrl) return gateway.Query(queryPointAllResults); }); - var queryPointExtentResults = new Query(serviceUrl.AsEndpoint()) + var queryPointExtentResults = new Query(serviceUrl.AsEndpoint()) { Geometry = new Extent { XMin = 0, YMin = 0, XMax = 180, YMax = -90, SpatialReference = SpatialReference.WGS84 }, // SE quarter of globe OutputSpatialReference = SpatialReference.WebMercator }; + var resultPointExtentResults = await IntegrationTestFixture.TestPolicy.ExecuteAsync(() => { - return gateway.Query(queryPointExtentResults); + return gateway.Query(queryPointExtentResults); }); var rings = new Point[] @@ -531,13 +532,13 @@ async Task QueryGeometryCriteriaHonored(string serviceUrl) new Point { X = 0, Y = 0 } }.ToPointCollectionList(); - var queryPointPolygonResults = new Query(serviceUrl.AsEndpoint()) + var queryPointPolygonResults = new Query(serviceUrl.AsEndpoint()) { Geometry = new Polygon { Rings = rings } }; var resultPointPolygonResults = await IntegrationTestFixture.TestPolicy.ExecuteAsync(() => { - return gateway.Query(queryPointPolygonResults); + return gateway.Query(queryPointPolygonResults); }); countAllResults = resultPointAllResults.Features.Count(); diff --git a/tests/Anywhere.ArcGIS.Test/GeoJSONTests.cs b/tests/Anywhere.ArcGIS.Test/GeoJSONTests.cs new file mode 100644 index 0000000..5cdb8b9 --- /dev/null +++ b/tests/Anywhere.ArcGIS.Test/GeoJSONTests.cs @@ -0,0 +1,56 @@ +using Anywhere.ArcGIS.Common; +using Anywhere.ArcGIS.GeoJson; +using Newtonsoft.Json; +using System.Linq; +using Xunit; + +namespace Anywhere.ArcGIS.Test +{ + public class GeoJSONTests + { + [Fact] + public void CanConvertToFeatures() + { + var start = "{ \"type\": \"FeatureCollection\", \"features\": [ { \"type\": \"Feature\", \"geometry\": "; + var end = "} ] }"; + + var pointData = "{ \"type\": \"Point\", \"coordinates\": [-77.038193, 38.901345] }"; + Convert(start + pointData + end); + + var lineData = "{ \"type\": \"LineString\", \"coordinates\": [ [100.0, 0.0], [101.0, 1.0] ] }"; + Convert(start + lineData + end); + + Convert(start + lineData.Replace("LineString", "MultiPoint") + end); + + var polygonData = "{ \"type\": \"Polygon\", \"coordinates\": [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ] ] }"; + Convert(start + polygonData + end); + + Convert(start + polygonData.Replace("Polygon", "MultiLineString") + end); + + var polygonData2 = "{ \"type\": \"Polygon\", \"coordinates\": [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ], [ [100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2] ] ] }"; + Convert(start + polygonData2 + end); + + Convert(start + polygonData2.Replace("Polygon", "MultiLineString") + end); + + var multiPolygonData = "{ \"type\": \"Polygon\", \"coordinates\": [ [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]], [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]], [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]] ] }"; + + Convert(start + multiPolygonData + end); + } + + void Convert(string data) + where TGeoJSON : IGeoJsonGeometry + where TGeometry : IGeometry + { + var featureCollection = JsonConvert.DeserializeObject>(data); + + Assert.NotNull(featureCollection); + + var features = featureCollection.ToFeatures(); + + Assert.NotNull(features); + Assert.Equal(featureCollection.Features.Count, features.Count); + Assert.IsType(features.First().Geometry); + Assert.True(features.All(f => f.Geometry != null)); + } + } +}