Skip to content

Commit 04b466d

Browse files
author
Jack Bond
authored
Expand spatial types supported by MicrosoftSpatialGeoJsonConverter. (#24520)
* Expand spatial types supported by MicrosoftSpatialGeoJsonConverter. * Added documentation. Cleaned up test data. * Expanded unit test discovered minor issue * Added Utf8JsonReaderExtensions which consolidates methods which extra data from a Utf8JsonReader. Tweaked comments. * Removed SkipComments. Tweaked exception messages. Updated Utf8JsonReaderExtensions method names.
1 parent 253e98d commit 04b466d

12 files changed

+1177
-59
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.Core.Serialization
5+
{
6+
/// <summary>
7+
/// The constants defined by the GeoJson standard
8+
/// A similar class exists in Microsoft.Spatial, unfortunately it is marked as internal.
9+
/// </summary>
10+
internal static class GeoJsonConstants
11+
{
12+
public const string PointTypeName = "Point";
13+
public const string LineStringTypeName = "LineString";
14+
public const string PolygonTypeName = "Polygon";
15+
public const string MultiPointTypeName = "MultiPoint";
16+
public const string MultiLineStringTypeName = "MultiLineString";
17+
public const string MultiPolygonTypeName = "MultiPolygon";
18+
public const string GeometryCollectionTypeName = "GeometryCollection";
19+
public const string CoordinatesPropertyName = "coordinates";
20+
public const string TypePropertyName = "type";
21+
public const string GeometriesPropertyName = "geometries";
22+
}
23+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Spatial;
5+
using System.Text.Json;
6+
7+
namespace Azure.Core.Serialization
8+
{
9+
/// <summary>
10+
/// Advances a Utf8JsonReader positioned at coordinates property to identify GeoJson type.
11+
/// </summary>
12+
/// <remarks>
13+
/// The GeoJson standard essentially has four "schemas" for containing geography coordinates.
14+
/// <list type="bullet" >
15+
/// <item>
16+
/// <description>Level zero defines a Point an array of doubles for latitude, longitude, etc.</description>
17+
/// </item>
18+
/// <item>
19+
/// <description>
20+
/// Level one, defines a LineString or MultiPoint, both which are an array of points.
21+
/// </description>
22+
/// </item>
23+
/// <item>
24+
/// <description>
25+
/// Level two, defines a Polygon or MultiLineString, an array of an array of points.
26+
/// </description>
27+
/// </item>
28+
/// <item>
29+
/// <description>
30+
/// Level three, defines a MultiPolygon, an array of an array of an array of points.
31+
/// </description>
32+
/// </item>
33+
/// </list>
34+
/// </remarks>
35+
internal abstract class GeoJsonCoordinateReader
36+
{
37+
public abstract void Process(ref Utf8JsonReader reader);
38+
39+
/// <summary>
40+
/// Accepts the GeoJson coordinates property, and iterates the top level array to determine
41+
/// which of the four schemas the coordinates match.
42+
/// </summary>
43+
/// <param name="reader">A Utf8JsonReader positioned at the GeoJson coordinates property.</param>
44+
/// <returns>A reader which will parse out correct parser based on the schema detected.</returns>
45+
/// <exception cref="JsonException"></exception>
46+
public static GeoJsonCoordinateReader Create(ref Utf8JsonReader reader)
47+
{
48+
GeoJsonCoordinateReader result;
49+
50+
reader.Expect(JsonTokenType.StartArray);
51+
52+
int detectedLevel = 0;
53+
54+
// How many nested StartArray tokens we find identifies which of the four schemas we're dealing with
55+
for (; reader.Read() && reader.TokenType == JsonTokenType.StartArray; detectedLevel++)
56+
{
57+
if (detectedLevel == 4)
58+
{
59+
throw new JsonException($"Deserialization failed. GeoJson property '{GeoJsonConstants.CoordinatesPropertyName}' does not contain recognizable GeoJson.");
60+
}
61+
}
62+
63+
reader.Expect(JsonTokenType.Number);
64+
65+
switch (detectedLevel)
66+
{
67+
case 0:
68+
result = new LevelZeroGeoJsonCoordinateReader();
69+
break;
70+
71+
case 1:
72+
result = new LevelOneGeoJsonCoordinateReader();
73+
break;
74+
75+
case 2:
76+
result = new LevelTwoGeoJsonCoordinateReader();
77+
break;
78+
79+
default:
80+
result = new LevelThreeGeoJsonCoordinateReader();
81+
break;
82+
}
83+
84+
result.Process(ref reader);
85+
86+
return result;
87+
}
88+
89+
/// <summary>
90+
/// The Geography a derived class read from a Utf8JsonReader. Some schema levels support more
91+
/// than one Geography type, e.g. level 1 supports LineString and MultiPoint, which are both an
92+
/// array of points. The type argument informs which of the two types to create and return.
93+
/// </summary>
94+
/// <param name="type">The type read from the GeoJson type property.</param>
95+
/// <returns>The Geography parsed by a level 0,1,2,3 reader.</returns>
96+
public abstract Geography GetGeography(string type);
97+
}
98+
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Spatial;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text.Json;
8+
9+
namespace Azure.Core.Serialization
10+
{
11+
/// <summary>
12+
/// A collection of extension methods for GeographyFactory. Unfortunately, GeographyFactory does not contain overloads
13+
/// for adding existing Geographies during the Build process. For example, you cannot simply add a GeographyPolygon to
14+
/// a GeographyMultiPolygon. Instead you must enumerate an existing GeographyPolygon's rings and add them iteratively
15+
/// while creating a GeographyMultiPolygon. The methods of this class properly iterate creating Geographies which are
16+
/// composed of other Geographies.
17+
/// </summary>
18+
internal static class GeographyFactoryExtensions
19+
{
20+
public static GeographyFactory<GeographyCollection> Add(this GeographyFactory<GeographyCollection> factory, Geography geography)
21+
{
22+
if (geography is GeographyPoint point)
23+
{
24+
factory = factory.Add(point);
25+
}
26+
27+
else if (geography is GeographyLineString lineString)
28+
{
29+
factory = factory.Add(lineString);
30+
}
31+
32+
else if (geography is GeographyPolygon polygon)
33+
{
34+
factory = factory.Add(polygon);
35+
}
36+
37+
else if (geography is GeographyMultiPoint multiPoint)
38+
{
39+
factory = factory.Add(multiPoint);
40+
}
41+
42+
else if (geography is GeographyMultiLineString multiLineString)
43+
{
44+
factory = factory.Add(multiLineString);
45+
}
46+
47+
else if (geography is GeographyMultiPolygon multiPolygon)
48+
{
49+
factory = factory.Add(multiPolygon);
50+
}
51+
52+
else if (geography is GeographyCollection geographyCollection)
53+
{
54+
factory = factory.Add(geographyCollection);
55+
}
56+
57+
return factory;
58+
}
59+
60+
private static GeographyFactory<GeographyCollection> Add(this GeographyFactory<GeographyCollection> factory, GeographyPoint point)
61+
{
62+
factory = factory.Point(point.Latitude, point.Longitude);
63+
64+
return factory;
65+
}
66+
67+
private static GeographyFactory<GeographyCollection> Add(this GeographyFactory<GeographyCollection> factory, GeographyLineString lineString)
68+
{
69+
factory = factory.LineString();
70+
71+
foreach (GeographyPoint point in lineString.Points)
72+
{
73+
factory = factory.LineTo(point.Latitude, point.Longitude);
74+
}
75+
76+
return factory;
77+
}
78+
79+
private static GeographyFactory<GeographyCollection> Add(this GeographyFactory<GeographyCollection> factory, GeographyPolygon polygon)
80+
{
81+
factory = factory.Polygon();
82+
83+
foreach (GeographyLineString ring in polygon.Rings)
84+
{
85+
factory = factory.Ring(ring.Points[0].Latitude, ring.Points[0].Longitude);
86+
87+
for (int i = 1; i < ring.Points.Count; i++)
88+
{
89+
factory = factory.LineTo(ring.Points[i].Latitude, ring.Points[i].Longitude);
90+
}
91+
}
92+
93+
return factory;
94+
}
95+
96+
private static GeographyFactory<GeographyCollection> Add(this GeographyFactory<GeographyCollection> factory, GeographyMultiPoint multiPoint)
97+
{
98+
foreach (GeographyPoint point in multiPoint.Points)
99+
{
100+
factory = factory.Add(point);
101+
}
102+
103+
return factory;
104+
}
105+
106+
private static GeographyFactory<GeographyCollection> Add(this GeographyFactory<GeographyCollection> factory, GeographyMultiLineString multiLineString)
107+
{
108+
foreach (GeographyLineString lineString in multiLineString.LineStrings)
109+
{
110+
factory = factory.Add(lineString);
111+
}
112+
113+
return factory;
114+
}
115+
116+
private static GeographyFactory<GeographyCollection> Add(this GeographyFactory<GeographyCollection> factory, GeographyMultiPolygon multiPolygon)
117+
{
118+
foreach (GeographyPolygon polygon in multiPolygon.Polygons)
119+
{
120+
factory = factory.Add(polygon);
121+
}
122+
123+
return factory;
124+
}
125+
126+
private static GeographyFactory<GeographyCollection> Add(this GeographyFactory<GeographyCollection> factory, GeographyCollection geographyCollection)
127+
{
128+
factory = factory.Collection();
129+
130+
foreach (Geography geography in geographyCollection.Geographies)
131+
{
132+
factory = factory.Add(geography);
133+
}
134+
135+
return factory;
136+
}
137+
138+
public static GeographyLineString Create(this GeographyFactory<GeographyLineString> factory, List<GeographyPoint> points)
139+
{
140+
if (points.Count < 2)
141+
{
142+
throw new JsonException($"Deserialization of {nameof(GeographyLineString)} failed. {GeoJsonConstants.LineStringTypeName} must contain at least two points.");
143+
}
144+
145+
foreach (GeographyPoint point in points)
146+
{
147+
factory = factory.LineTo(point.Latitude, point.Longitude);
148+
}
149+
150+
GeographyLineString result = factory.Build();
151+
152+
return result;
153+
}
154+
155+
public static GeographyPolygon Create(this GeographyFactory<GeographyPolygon> factory, List<List<GeographyPoint>> listOfPointList)
156+
{
157+
foreach (List<GeographyPoint> pointList in listOfPointList)
158+
{
159+
if (pointList.Count < 4)
160+
{
161+
throw new JsonException($"Deserialization of {nameof(GeographyPolygon)} failed. {GeoJsonConstants.PolygonTypeName} must have at least four points.");
162+
}
163+
164+
else if (!pointList.First().Equals(pointList.Last()))
165+
{
166+
throw new JsonException($"Deserialization of {nameof(GeographyPolygon)} failed. {GeoJsonConstants.PolygonTypeName} first and last point must be the same.");
167+
}
168+
169+
factory = factory.Ring(pointList[0].Latitude, pointList[0].Longitude);
170+
171+
for (int i = 1; i < pointList.Count - 1; i++)
172+
{
173+
factory = factory.LineTo(pointList[i].Latitude, pointList[i].Longitude);
174+
}
175+
}
176+
177+
var result = factory.Build();
178+
179+
return result;
180+
}
181+
182+
public static GeographyMultiPoint Create(this GeographyFactory<GeographyMultiPoint> factory, List<GeographyPoint> points)
183+
{
184+
foreach (GeographyPoint point in points)
185+
{
186+
factory = factory.Point(point.Latitude, point.Longitude);
187+
}
188+
189+
GeographyMultiPoint result = factory.Build();
190+
191+
return result;
192+
}
193+
194+
public static GeographyMultiLineString Create(this GeographyFactory<GeographyMultiLineString> factory, List<List<GeographyPoint>> listOfPointList)
195+
{
196+
foreach (List<GeographyPoint> points in listOfPointList)
197+
{
198+
if (points.Count < 2)
199+
{
200+
throw new JsonException($"Deserialization of {nameof(GeographyMultiLineString)} failed. {GeoJsonConstants.MultiLineStringTypeName} must contain at least two points.");
201+
}
202+
203+
factory = factory.LineString();
204+
205+
foreach (GeographyPoint point in points)
206+
{
207+
factory = factory.LineTo(point.Latitude, point.Longitude);
208+
}
209+
}
210+
211+
GeographyMultiLineString result = factory.Build();
212+
213+
return result;
214+
}
215+
216+
public static GeographyMultiPolygon Create(this GeographyFactory<GeographyMultiPolygon> factory, List<List<List<GeographyPoint>>> points)
217+
{
218+
foreach (List<List<GeographyPoint>> polygon in points)
219+
{
220+
factory = factory.Polygon();
221+
222+
foreach (List<GeographyPoint> ring in polygon)
223+
{
224+
factory = factory.Ring(ring[0].Latitude, ring[0].Longitude);
225+
226+
for (int i = 1; i < ring.Count - 1; i++)
227+
{
228+
factory = factory.LineTo(ring[i].Latitude, ring[i].Longitude);
229+
}
230+
}
231+
}
232+
233+
GeographyMultiPolygon result = factory.Build();
234+
235+
return result;
236+
}
237+
238+
public static GeographyCollection Create(this GeographyFactory<GeographyCollection> factory, List<Geography> geographies)
239+
{
240+
foreach (Geography geography in geographies)
241+
{
242+
factory.Add(geography);
243+
}
244+
245+
return factory.Build();
246+
}
247+
}
248+
}

0 commit comments

Comments
 (0)