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 }