1 // Copyright 2018 Google Inc. All Rights Reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS-IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 //
15 
16 module s2.s2text_format;
17 
18 // s2text_format contains a collection of functions for converting
19 // geometry to and from a human-readable format.  It is mainly
20 // intended for testing and debugging.  Be aware that the
21 // human-readable format is *not* designed to preserve the full
22 // precision of the original object, so it should not be used
23 // for data storage.
24 
25 import s2.mutable_s2shape_index;
26 import s2.s2latlng;
27 import s2.s2latlng_rect;
28 import s2.s2lax_polygon_shape;
29 import s2.s2lax_polyline_shape;
30 import s2.s2loop;
31 import s2.s2point;
32 import s2.s2point_vector_shape;
33 import s2.s2polygon;
34 import s2.s2polyline;
35 import s2.s2shape;
36 import s2.s2shape_index;
37 import s2.strings.serialize;
38 
39 import std.conv;
40 import std.exception;
41 import std.format : format;
42 import std.range;
43 import std.string;
44 
45 // Returns an S2Point corresponding to the given a latitude-longitude
46 // coordinate in degrees.  Example of the input format:
47 //     "-20:150"
48 S2Point makePointOrDie(string str) {
49   S2Point point;
50   enforce(makePoint(str, point), ": str == \"" ~ str ~ "\"");
51   return point;
52 }
53 
54 // As above, but do not CHECK-fail on invalid input. Returns true if conversion
55 // is successful.
56 bool makePoint(string str, ref S2Point point) {
57   S2Point[] vertices;
58   if (!parsePoints(str, vertices) || vertices.length != 1) return false;
59   point = vertices[0];
60   return true;
61 }
62 
63 deprecated("Use MakePointOrDie.")
64 S2Point makePoint(string str) {
65   return makePointOrDie(str);
66 }
67 
68 // Parses a string of one or more latitude-longitude coordinates in degrees,
69 // and return the corresponding vector of S2LatLng points.
70 // Examples of the input format:
71 //     ""                            // no points
72 //     "-20:150"                     // one point
73 //     "-20:150, -20:151, -19:150"   // three points
74 S2LatLng[] parseLatLngsOrDie(string str) {
75   S2LatLng[] latlngs;
76   enforce(parseLatLngs(str, latlngs), ": str == \"" ~ str ~ "\"");
77   return latlngs;
78 }
79 
80 // As above, but does not CHECK-fail on invalid input. Returns true if
81 // conversion is successful.
82 bool parseLatLngs(string str, ref S2LatLng[] latlngs) {
83   string[2][] ps;
84   if (!dictionaryParse(str, ps)) return false;
85   foreach (p; ps) {
86     try {
87       double lat = to!double(strip(p[0]));
88       double lng = to!double(strip(p[1]));
89       latlngs ~= S2LatLng.fromDegrees(lat, lng);
90     } catch (ConvException e) {
91       return false;
92     }
93   }
94   return true;
95 }
96 
97 deprecated("Use ParseLatLngsOrDie.")
98 S2LatLng[] parseLatLngs(string str) {
99   return parseLatLngsOrDie(str);
100 }
101 
102 // Parses a string in the same format as ParseLatLngs, and return the
103 // corresponding vector of S2Point values.
104 S2Point[] parsePointsOrDie(string str) {
105   S2Point[] vertices;
106   enforce(parsePoints(str, vertices), ": str == \"" ~ str ~ "\"");
107   return vertices;
108 }
109 
110 // As above, but does not CHECK-fail on invalid input. Returns true if
111 // conversion is successful.
112 bool parsePoints(string str, ref S2Point[] vertices) {
113   S2LatLng[] latlngs;
114   if (!parseLatLngs(str, latlngs)) return false;
115   foreach (latlng; latlngs) {
116     vertices ~= latlng.toS2Point();
117   }
118   return true;
119 }
120 
121 deprecated("Use ParsePointsOrDie.")
122 S2Point[] parsePoints(string str) {
123   return parsePointsOrDie(str);
124 }
125 
126 bool makeLatLng(string str, ref S2LatLng latlng) {
127   S2LatLng[] latlngs;
128   if (!parseLatLngs(str, latlngs) || latlngs.length != 1) return false;
129   latlng = latlngs[0];
130   return true;
131 }
132 
133 // Given a string in the same format as ParseLatLngs, returns a single S2LatLng
134 S2LatLng makeLatLngOrDie(string str) {
135   S2LatLng latlng;
136   enforce(makeLatLng(str, latlng), ": str == \"" ~ str ~ "\"");
137   return latlng;
138 }
139 
140 // Given a string in the same format as ParseLatLngs, returns the minimal
141 // bounding S2LatLngRect that contains the coordinates.
142 S2LatLngRect makeLatLngRectOrDie(string str) {
143   S2LatLngRect rect;
144   enforce(makeLatLngRect(str, rect), ": str == \"" ~ str ~ "\"");
145   return rect;
146 }
147 
148 // As above, but does not CHECK-fail on invalid input. Returns true if
149 // conversion is successful.
150 bool makeLatLngRect(string str, ref S2LatLngRect rect) {
151   S2LatLng[] latlngs;
152   if (!parseLatLngs(str, latlngs) || latlngs.empty()) return false;
153   rect = S2LatLngRect.fromPoint(latlngs[0]);
154   for (int i = 1; i < latlngs.length; ++i) {
155     rect.addPoint(latlngs[i]);
156   }
157   return true;
158 }
159 
160 deprecated("Use MakeLatLngRectOrDie.")
161 S2LatLngRect makeLatLngRect(string str) {
162   return makeLatLngRectOrDie(str);
163 }
164 
165 // Given a string of latitude-longitude coordinates in degrees,
166 // returns a newly allocated loop.  Example of the input format:
167 //     "-20:150, 10:-120, 0.123:-170.652"
168 // The strings "empty" or "full" create an empty or full loop respectively.
169 S2Loop makeLoopOrDie(string str) {
170   S2Loop loop;
171   enforce(makeLoop(str, loop), ": str == \"" ~ str ~ "\"");
172   return loop;
173 }
174 
175 // As above, but does not CHECK-fail on invalid input. Returns true if
176 // conversion is successful.
177 bool makeLoop(string str, ref S2Loop loop) {
178   if (str == "empty") {
179     loop = new S2Loop(S2Loop.empty());
180     return true;
181   }
182   if (str == "full") {
183     loop = new S2Loop(S2Loop.full());
184     return true;
185   }
186   S2Point[] vertices;
187   if (!parsePoints(str, vertices)) return false;
188   loop = new S2Loop(vertices);
189   return true;
190 }
191 
192 deprecated("Use MakeLoopOrDie.")
193 S2Loop makeLoop(string str) {
194   return makeLoopOrDie(str);
195 }
196 
197 // Similar to MakeLoop(), but returns an S2Polyline rather than an S2Loop.
198 S2Polyline makePolylineOrDie(string str) {
199   S2Polyline polyline;
200   enforce(makePolyline(str, polyline), ": str == \"" ~ str ~ "\"");
201   return polyline;
202 }
203 
204 // As above, but does not CHECK-fail on invalid input. Returns true if
205 // conversion is successful.
206 bool makePolyline(string str, ref S2Polyline polyline) {
207   S2Point[] vertices;
208   if (!parsePoints(str, vertices)) return false;
209   polyline = new S2Polyline(vertices);
210   return true;
211 }
212 
213 deprecated("Use MakePolylineOrDie.")
214 S2Polyline makePolyline(string str) {
215   return makePolylineOrDie(str);
216 }
217 
218 // Like MakePolyline, but returns an S2LaxPolylineShape instead.
219 S2LaxPolylineShape makeLaxPolylineOrDie(string str) {
220   auto lax_polyline = new S2LaxPolylineShape();
221   enforce(makeLaxPolyline(str, lax_polyline), ": str == \"" ~ str ~ "\"");
222   return lax_polyline;
223 }
224 
225 // As above, but does not CHECK-fail on invalid input. Returns true if
226 // conversion is successful.
227 bool makeLaxPolyline(string str, ref S2LaxPolylineShape lax_polyline) {
228   S2Point[] vertices;
229   if (!parsePoints(str, vertices)) return false;
230   lax_polyline = new S2LaxPolylineShape(vertices);
231   return true;
232 }
233 
234 deprecated("Use MakeLaxPolylineOrDie.")
235 S2LaxPolylineShape makeLaxPolyline(string str) {
236   return makeLaxPolylineOrDie(str);
237 }
238 
239 // Given a sequence of loops separated by semicolons, returns a newly
240 // allocated polygon.  Loops are automatically normalized by inverting them
241 // if necessary so that they enclose at most half of the unit sphere.
242 // (Historically this was once a requirement of polygon loops.  It also
243 // hides the problem that if the user thinks of the coordinates as X:Y
244 // rather than LAT:LNG, it yields a loop with the opposite orientation.)
245 //
246 // Examples of the input format:
247 //     "10:20, 90:0, 20:30"                                  // one loop
248 //     "10:20, 90:0, 20:30; 5.5:6.5, -90:-180, -15.2:20.3"   // two loops
249 //     ""       // the empty polygon (consisting of no loops)
250 //     "empty"  // the empty polygon (consisting of no loops)
251 //     "full"   // the full polygon (consisting of one full loop).
252 S2Polygon makePolygonOrDie(string str) {
253   S2Polygon polygon;
254   enforce(internalMakePolygon(str, true, polygon), ": str == \"" ~ str ~ "\"");
255   return polygon;
256 }
257 
258 // As above, but does not CHECK-fail on invalid input. Returns true if
259 // conversion is successful.
260 bool makePolygon(string str, ref S2Polygon polygon) {
261   return internalMakePolygon(str, true, polygon);
262 }
263 
264 private bool internalMakePolygon(string str, bool normalize_loops, ref S2Polygon polygon) {
265   polygon = new S2Polygon();
266   if (str == "empty") str = "";
267   string[] loop_strs = str.split(';');
268   S2Loop[] loops;
269   foreach (loop_str; loop_strs) {
270     loop_str = strip(loop_str);
271     if (loop_str.empty()) break;
272 
273     S2Loop loop;
274     if (!makeLoop(loop_str, loop)) return false;
275     // Don't normalize loops that were explicitly specified as "full".
276     if (normalize_loops && !loop.isFull()) loop.normalize();
277     loops ~= loop;
278   }
279   polygon = new S2Polygon(loops);
280   return true;
281 }
282 
283 
284 /**
285  * Like MakePolygon(), except that it does not normalize loops (i.e., it
286  * gives you exactly what you asked for).
287  */
288 S2Polygon makeVerbatimPolygonOrDie(string str) {
289   S2Polygon polygon;
290   enforce(makeVerbatimPolygon(str, polygon), ": str == \"" ~ str ~ "\"");
291   return polygon;
292 }
293 
294 /**
295  * As above, but does not CHECK-fail on invalid input. Returns true if
296  * conversion is successful.
297  */
298 bool makeVerbatimPolygon(string str, out S2Polygon polygon) {
299   return internalMakePolygon(str, false, polygon);
300 }
301 
302 deprecated("Use MakeVerbatimPolygonOrDie.")
303 S2Polygon makeVerbatimPolygon(string str) {
304   return makeVerbatimPolygonOrDie(str);
305 }
306 
307 // Parses a string in the same format as MakePolygon, except that loops must
308 // be oriented so that the interior of the loop is always on the left, and
309 // polygons with degeneracies are supported.  As with MakePolygon, "full" and
310 // denotes the full polygon and "" or "empty" denote the empty polygon.
311 S2LaxPolygonShape makeLaxPolygonOrDie(string str) {
312   S2LaxPolygonShape lax_polygon;
313   enforce(makeLaxPolygon(str, lax_polygon), ": str == \"" ~ str ~ "\"");
314   return lax_polygon;
315 }
316 
317 // As above, but does not CHECK-fail on invalid input. Returns true if
318 // conversion is successful.
319 bool makeLaxPolygon(string str, ref S2LaxPolygonShape lax_polygon) {
320   string[] loop_strs = str.split(";");
321   S2Point[][] loops;
322   foreach (loop_str; loop_strs) {
323     loop_str = strip(loop_str);
324     if (loop_str.empty()) break;
325 
326     if (loop_str == "full") {
327       loops ~= new S2Point[0];
328     } else if (loop_str != "empty") {
329       S2Point[] points;
330       if (!parsePoints(loop_str, points)) return false;
331       loops ~= points;
332     }
333   }
334   lax_polygon = new S2LaxPolygonShape(loops);
335   return true;
336 }
337 
338 deprecated("Use MakeLaxPolygonOrDie.")
339 S2LaxPolygonShape makeLaxPolygon(string str) {
340   return makeLaxPolygonOrDie(str);
341 }
342 
343 // Returns a MutableS2ShapeIndex containing the points, polylines, and loops
344 // (in the form of a single polygon) described by the following format:
345 //
346 //   point1|point2|... # line1|line2|... # polygon1|polygon2|...
347 //
348 // Examples:
349 //   1:2 | 2:3 # #                     // Two points
350 //   # 0:0, 1:1, 2:2 | 3:3, 4:4 #      // Two polylines
351 //   # # 0:0, 0:3, 3:0; 1:1, 2:1, 1:2  // Two nested loops (one polygon)
352 //   5:5 # 6:6, 7:7 # 0:0, 0:1, 1:0    // One of each
353 //   # # empty                         // One empty polygon
354 //   # # empty | full                  // One empty polygon, one full polygon
355 //
356 // Loops should be directed so that the region's interior is on the left.
357 // Loops can be degenerate (they do not need to meet S2Loop requirements).
358 //
359 // CAVEAT: Because whitespace is ignored, empty polygons must be specified
360 //         as the string "empty" rather than as the empty string ("").
361 MutableS2ShapeIndex makeIndexOrDie(string str) {
362   auto index = new MutableS2ShapeIndex();
363   enforce(makeIndex(str, index), ": str == \"" ~ str ~ "\"");
364   return index;
365 }
366 
367 // As above, but does not CHECK-fail on invalid input. Returns true if
368 // conversion is successful.
369 bool makeIndex(string str, ref MutableS2ShapeIndex index) {
370   string[] strs = str.split('#');
371   enforce(strs.length == 3, "Must contain two # characters: " ~ str);
372 
373   S2Point[] points;
374   foreach (point_str; strs[0].strip().split('|')) {
375     point_str = strip(point_str);
376     if (point_str.empty()) break;
377 
378     S2Point point;
379     if (!makePoint(point_str, point)) return false;
380     points ~= point;
381   }
382   if (!points.empty()) {
383     index.add(new S2PointVectorShape(points));
384   }
385   foreach (line_str; strs[1].strip().split('|')) {
386     auto lax_polyline = new S2LaxPolylineShape();
387     if (!makeLaxPolyline(line_str, lax_polyline)) return false;
388     index.add(lax_polyline);
389   }
390   foreach (polygon_str; strs[2].strip().split('|')) {
391     auto lax_polygon = new S2LaxPolygonShape();
392     if (!makeLaxPolygon(polygon_str, lax_polygon)) return false;
393     index.add(lax_polygon);
394   }
395   return true;
396 }
397 
398 deprecated("Use MakeIndexOrDie.")
399 MutableS2ShapeIndex makeIndex(string str) {
400   return makeIndexOrDie(str);
401 }
402 
403 private void appendVertex(in S2LatLng ll, ref string val) {
404   val ~= format("%.15g:%.15g", ll.lat().degrees(), ll.lng().degrees());
405 }
406 
407 private void appendVertex(in S2Point p, ref string val) {
408   auto ll = S2LatLng(p);
409   return appendVertex(ll, val);
410 }
411 
412 private void appendVertices(in S2Point[] v, ref string val) {
413   for (int i = 0; i < v.length; ++i) {
414     if (i > 0) val ~= ", ";
415     appendVertex(v[i], val);
416   }
417 }
418 
419 string toString(in S2Point point) {
420   string val;
421   appendVertex(point, val);
422   return val;
423 }
424 
425 string toString(in S2LatLngRect rect) {
426   string val;
427   appendVertex(rect.lo(), val);
428   val ~= ", ";
429   appendVertex(rect.hi(), val);
430   return val;
431 }
432 
433 string toString(in S2LatLng latlng) {
434   string val;
435   appendVertex(latlng, val);
436   return val;
437 }
438 
439 string toString(in S2Loop loop) {
440   if (loop.isEmpty()) {
441     return "empty";
442   } else if (loop.isFull()) {
443     return "full";
444   }
445   string val;
446   if (loop.numVertices() > 0) {
447     appendVertices(loop.vertices(), val);
448   }
449   return val;
450 }
451 
452 string toString(in S2Polyline polyline) {
453   string val;
454   if (polyline.numVertices() > 0) {
455     appendVertices(polyline.vertices(), val);
456   }
457   return val;
458 }
459 
460 string toString(in S2Polygon polygon) {
461   if (polygon.isEmpty()) {
462     return "empty";
463   } else if (polygon.isFull()) {
464     return "full";
465   }
466   string val;
467   for (int i = 0; i < polygon.numLoops(); ++i) {
468     if (i > 0) val ~= ";\n";
469     const(S2Loop) loop = polygon.loop(i);
470     appendVertices(loop.vertices(), val);
471   }
472   return val;
473 }
474 
475 string toString(in S2Point[] points) {
476   string val;
477   appendVertices(points, val);
478   return val;
479 }
480 
481 string toString(in S2LatLng[] latlngs) {
482   string val;
483   for (int i = 0; i < latlngs.length; ++i) {
484     if (i > 0) val ~= ", ";
485     appendVertex(latlngs[i], val);
486   }
487   return val;
488 }
489 
490 string toString(in S2LaxPolylineShape polyline) {
491   string val;
492   if (polyline.numVertices() > 0) {
493     appendVertices(polyline.vertices(), val);
494   }
495   return val;
496 }
497 
498 string toString(in S2LaxPolygonShape polygon) {
499   string val;
500   for (int i = 0; i < polygon.numLoops(); ++i) {
501     if (i > 0) val ~= ";\n";
502     int n = polygon.numLoopVertices(i);
503     if (n > 0) appendVertices(polygon.loopVertices(i), val);
504   }
505   return val;
506 }
507 
508 // Convert the contents of an S2ShapeIndex to the format above.  The index may
509 // contain S2Shapes of any type.  Shapes are reordered if necessary so that
510 // all point geometry (shapes of dimension 0) are first, followed by all
511 // polyline geometry, followed by all polygon geometry.
512 // string ToString(const S2ShapeIndex& index);
513 string toString(in S2ShapeIndex index) {
514   string val;
515   for (int dim = 0; dim < 3; ++dim) {
516     if (dim > 0) val ~= "#";
517     int count = 0;
518     for (int s = 0; s < index.numShapeIds(); ++s) {
519       const(S2Shape) shape = index.shape(s);
520       if (shape is null || shape.dimension() != dim) continue;
521       val ~= (count > 0) ? " | " : (dim > 0) ? " " : "";
522       for (int i = 0; i < shape.numChains(); ++i, ++count) {
523         if (i > 0) val ~= (dim == 2) ? "; " : " | ";
524         S2Shape.Chain chain = shape.chain(i);
525         appendVertex(shape.edge(chain.start).v0, val);
526         int limit = chain.start + chain.length;
527         if (dim != 1) --limit;
528         for (int e = chain.start; e < limit; ++e) {
529           val ~= ", ";
530           appendVertex(shape.edge(e).v1, val);
531         }
532       }
533     }
534     // Example output: "# #", "0:0 # #", "# # 0:0, 0:1, 1:0"
535     if (dim == 1 || (dim == 0 && count > 0)) val ~= " ";
536   }
537   return val;
538 }