1 // Copyright 2014 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 * Convert locations to and from convenient short codes. 17 * 18 * Open Location Codes are short, ~10 character codes that can be used instead of street 19 * addresses. The codes can be generated and decoded offline, and use a reduced character set that 20 * minimises the chance of codes including words. 21 * 22 * Ported from the Java interface. 23 * 24 * @author Jiri Semecky 25 * @author Doug Rinckes 26 * @author Jan Jurzitza 27 */ 28 module openlocationcode; 29 @safe: 30 31 import std.algorithm; 32 import std.array; 33 import std.format; 34 import std.math; 35 import std..string; 36 37 /// Provides a normal precision code, approximately 14x14 meters. 38 enum codePrecisionNormal = 10; 39 40 /// The character set used to encode the values. 41 static immutable ubyte[] codeAlphabet = "23456789CFGHJMPQRVWX".representation; 42 43 /// A separator used to break the code into two parts to aid memorability. 44 enum separator = '+'; 45 46 /// The character used to pad codes. 47 enum paddingCharacter = '0'; 48 49 /// The number of characters to place before the separator. 50 private enum separatorPosition = 8; 51 52 /// The max number of digits to process in a plus code. 53 enum maxDigitCount = 15; 54 55 /// The max number of characters in an open location code. This is the max number of digits plus a separator character. 56 enum maxOLCLength = maxDigitCount + 1; 57 58 /// Maximum code length using just lat/lng pair encoding. 59 private enum pairCodeLength = 10; 60 61 /// Number of digits in the grid coding section. 62 private enum gridCodeLength = maxDigitCount - pairCodeLength; 63 64 /// The base to use to convert numbers to/from. 65 private enum encodingBase = cast(int) codeAlphabet.length; 66 67 /// The maximum value for latitude in degrees. 68 private enum long latitudeMax = 90; 69 70 /// The maximum value for longitude in degrees. 71 private enum long longitudeMax = 180; 72 73 /// Number of columns in the grid refinement method. 74 private enum gridColumns = 4; 75 76 /// Number of rows in the grid refinement method. 77 private enum gridRows = 5; 78 79 /// Value to multiple latitude degrees to convert it to an integer with the maximum encoding 80 /// precision. I.e. encodingBase**3 * gridRows**gridCodeLength 81 private enum long latIntegerMultiplier = 8000 * 3125; 82 83 /// Value to multiple longitude degrees to convert it to an integer with the maximum encoding 84 /// precision. I.e. encodingBase**3 * gridColumns**gridCodeLength 85 private enum long lngIntegerMultiplier = 8000 * 1024; 86 87 /// Value of the most significant latitude digit after it has been converted to an integer. 88 private enum long latMspValue = latIntegerMultiplier * encodingBase * encodingBase; 89 90 /// Value of the most significant longitude digit after it has been converted to an integer. 91 private enum long lngMspValue = lngIntegerMultiplier * encodingBase * encodingBase; 92 93 /** 94 * Coordinates of a decoded Open Location Code. 95 * 96 * The coordinates include the latitude and longitude of the lower left and upper right corners 97 * and the center of the bounding box for the area the code represents. 98 */ 99 struct OpenLocationCodeArea 100 { 101 @nogc nothrow pure: 102 private double _southLatitude = 0; 103 private double _westLongitude = 0; 104 private double _northLatitude = 0; 105 private double _eastLongitude = 0; 106 private int _length; 107 108 /// Creats this coordinate area from min/max longitude and latitude and a code length. 109 this(double southLatitude, double westLongitude, double northLatitude, 110 double eastLongitude, int length) 111 { 112 _southLatitude = southLatitude; 113 _westLongitude = westLongitude; 114 _northLatitude = northLatitude; 115 _eastLongitude = eastLongitude; 116 _length = length; 117 } 118 119 double southLatitude() const @property 120 { 121 return _southLatitude; 122 } 123 124 double westLongitude() const @property 125 { 126 return _westLongitude; 127 } 128 129 double latitudeHeight() const @property 130 { 131 return _northLatitude - _southLatitude; 132 } 133 134 double longitudeWidth() const @property 135 { 136 return _eastLongitude - _westLongitude; 137 } 138 139 double centerLatitude() const @property 140 { 141 return (_southLatitude + _northLatitude) * 0.5; 142 } 143 144 double centerLongitude() const @property 145 { 146 return (_westLongitude + _eastLongitude) * 0.5; 147 } 148 149 double northLatitude() const @property 150 { 151 return _northLatitude; 152 } 153 154 double eastLongitude() const @property 155 { 156 return _eastLongitude; 157 } 158 159 int length() const @property 160 { 161 return _length; 162 } 163 164 /// Returns: `true` if this area contains the given coordinate. 165 bool contains(double latitude, double longitude) const 166 { 167 return southLatitude <= latitude && latitude < northLatitude 168 && westLongitude <= longitude && longitude < eastLongitude; 169 } 170 } 171 172 /// Thrown in methods validating a user input string code. 173 class OLCFormatException : Exception 174 { 175 /// 176 this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null) pure nothrow @nogc @safe 177 { 178 super(msg, file, line, nextInChain); 179 } 180 } 181 182 /// Represents an open location code. 183 struct OpenLocationCode 184 { 185 pure: 186 /// The current code for the object 187 private immutable(ubyte)[] _code; // note: ubyte[] because it's only ascii 188 189 string code() const @property nothrow @nogc @trusted 190 { 191 return cast(string) _code; 192 } 193 194 /** 195 * Creates Open Location Code object for the provided code. 196 * 197 * Params: 198 * code = A valid OLC code. Can be a full code or a shortened code. 199 * Throws: $(D OLCFormatException) when the passed code is not valid. 200 */ 201 static OpenLocationCode fromString(string code) 202 { 203 code = code.toUpper; 204 if (!isValidCode(code)) 205 { 206 throw new OLCFormatException( 207 format!"The provided code '%s' is not a valid Open Location Code."(code)); 208 } 209 return fromTrustedString(code); 210 } 211 212 /** 213 * Creates Open Location Code object for the provided code performing no validation. This should call isValidCode before and abort if not valid. 214 * 215 * Params: 216 * code = A valid OLC code. Can be a full code or a shortened code. 217 */ 218 static OpenLocationCode fromTrustedString(string code) nothrow @nogc 219 { 220 // trim to max length, valid check might allow more characters 221 return OpenLocationCode(code.representation[0 .. min(maxOLCLength, $)]); 222 } 223 224 /** 225 * Creates Open Location Code. 226 * 227 * Params: 228 * buffer = The character buffer to write to. (ASCII only = ubyte[]) Should be at least maxOLCLength in size to avoid crashes. 229 * latitude = The latitude in decimal degrees. 230 * longitude = The longitude in decimal degrees. 231 * codeLength = The desired number of digits in the code. Defaults to the default precision. Must be a valid code length, otherwise contract violation = assertion error. 232 */ 233 static OpenLocationCode encode(double latitude, double longitude, 234 int codeLength = codePrecisionNormal) nothrow 235 { 236 ubyte[maxOLCLength] buffer; 237 auto ret = encode(buffer[], latitude, longitude, codeLength); 238 return OpenLocationCode(ret.idup); 239 } 240 241 /// ditto 242 static ubyte[] encode(scope return ubyte[] buffer, double latitude, 243 double longitude, int codeLength = codePrecisionNormal) nothrow @nogc 244 in(codeLength.isValidCodeLength, "Illegal code length") 245 { 246 // Limit the maximum number of digits in the code. 247 codeLength = min(codeLength, maxDigitCount); 248 // Ensure that latitude and longitude are valid. 249 latitude = clipLatitude(latitude); 250 longitude = normalizeLongitude(longitude); 251 252 // Latitude 90 needs to be adjusted to be just less, so the returned code can also be decoded. 253 if (latitude == latitudeMax) 254 { 255 latitude = latitude - 0.9 * computeLatitudePrecision(codeLength); 256 } 257 258 // Store the code - we build it in reverse and reorder it afterwards. 259 auto len = 0; 260 261 // Compute the code. 262 // This approach converts each value to an integer after multiplying it by 263 // the final precision. This allows us to use only integer operations, so 264 // avoiding any accumulation of floating point representation errors. 265 266 // Multiply values by their precision and convert to positive. Rounding 267 // avoids/minimises errors due to floating point precision. 268 long latVal = cast(long)(round((latitude + latitudeMax) * latIntegerMultiplier * 1e6) / 1e6); 269 long lngVal = cast(long)(round((longitude + longitudeMax) * lngIntegerMultiplier * 1e6) / 1e6); 270 271 // Compute the grid part of the code if necessary. 272 if (codeLength > pairCodeLength) 273 { 274 foreach (i; 0 .. gridCodeLength) 275 { 276 const latDigit = latVal % gridRows; 277 const lngDigit = lngVal % gridColumns; 278 int ndx = cast(int)(latDigit * gridColumns + lngDigit); 279 buffer[len++] = codeAlphabet[ndx]; 280 latVal /= gridRows; 281 lngVal /= gridColumns; 282 } 283 } 284 else 285 { 286 latVal = cast(long)(latVal / pow(gridRows, gridCodeLength)); 287 lngVal = cast(long)(lngVal / pow(gridColumns, gridCodeLength)); 288 } 289 // Compute the pair section of the code. 290 for (int i = 0; i < pairCodeLength / 2; i++) 291 { 292 buffer[len++] = codeAlphabet[cast(int)(lngVal % encodingBase)]; 293 buffer[len++] = codeAlphabet[cast(int)(latVal % encodingBase)]; 294 latVal /= encodingBase; 295 lngVal /= encodingBase; 296 // If we are at the separator position, add the separator. 297 if (i == 0) 298 { 299 buffer[len++] = separator; 300 } 301 } 302 // Reverse the code. 303 auto codeBuilder = buffer[0 .. len]; 304 codeBuilder.reverse(); 305 306 // If we need to pad the code, replace some of the digits. 307 if (codeLength < separatorPosition) 308 { 309 if (codeBuilder.length < separatorPosition) 310 { 311 codeBuilder = buffer[0 .. separatorPosition]; 312 codeBuilder[len .. $] = paddingCharacter; 313 } 314 else 315 { 316 codeBuilder[codeLength .. separatorPosition] = paddingCharacter; 317 } 318 } 319 return codeBuilder[0 .. max(separatorPosition + 1, codeLength + 1)]; 320 } 321 322 /** 323 * Decodes this object into a $(D OpenLocationCodeArea) object encapsulating latitude/longitude bounding box. This method doesn't allocate when not throwing an exception. 324 * 325 * Returns: A OpenLocationCodeArea object. 326 * 327 * Throws: $(D OLCFormatException) if this is not a valid full code. 328 */ 329 OpenLocationCodeArea decode() const 330 { 331 if (!isFull) 332 { 333 throw new OLCFormatException( 334 format!"Method decode() could only be called on valid full codes, code was '%s'."(code)); 335 } 336 return decodeTrustedFull(); 337 } 338 339 /// ditto 340 static OpenLocationCodeArea decode(string code) 341 { 342 return OpenLocationCode.fromString(code).decode(); 343 } 344 345 /** 346 * Decodes this object into a $(D OpenLocationCodeArea) object encapsulating latitude/longitude bounding box. This method doesn't allocate when not throwing an exception. 347 * 348 * Returns: A OpenLocationCodeArea object. 349 */ 350 OpenLocationCodeArea decodeTrustedFull() const nothrow @nogc 351 in(isFull) 352 { 353 // Strip padding and separator characters out of the code. 354 ubyte[maxOLCLength] buffer; 355 int buflen; 356 foreach (c; _code) 357 if (c != separator && c != paddingCharacter) 358 buffer[buflen++] = c; 359 auto code = buffer[0 .. buflen]; 360 361 // Initialise the values. We work them out as integers and convert them to doubles at the end. 362 long latVal = -latitudeMax * latIntegerMultiplier; 363 long lngVal = -longitudeMax * lngIntegerMultiplier; 364 // Define the place value for the digits. We'll divide this down as we work through the code. 365 long latPlaceVal = latMspValue; 366 long lngPlaceVal = lngMspValue; 367 for (int i = 0; i < min(code.length, pairCodeLength); i += 2) 368 { 369 latPlaceVal /= encodingBase; 370 lngPlaceVal /= encodingBase; 371 latVal += codeAlphabet.countUntil(code[i]) * latPlaceVal; 372 lngVal += codeAlphabet.countUntil(code[i + 1]) * lngPlaceVal; 373 } 374 for (int i = pairCodeLength; i < min(code.length, maxDigitCount); i++) 375 { 376 latPlaceVal /= gridRows; 377 lngPlaceVal /= gridColumns; 378 const digit = codeAlphabet.countUntil(code[i]); 379 const row = digit / gridColumns; 380 const col = digit % gridColumns; 381 latVal += row * latPlaceVal; 382 lngVal += col * lngPlaceVal; 383 } 384 const latitudeLo = cast(double) latVal / latIntegerMultiplier; 385 const longitudeLo = cast(double) lngVal / lngIntegerMultiplier; 386 const latitudeHi = cast(double)(latVal + latPlaceVal) / latIntegerMultiplier; 387 const longitudeHi = cast(double)(lngVal + lngPlaceVal) / lngIntegerMultiplier; 388 return OpenLocationCodeArea(latitudeLo, longitudeLo, latitudeHi, 389 longitudeHi, min(code.length, maxDigitCount)); 390 } 391 392 /** 393 * Returns whether this $(D OpenLocationCode) is a full Open Location Code. 394 * 395 * Returns: `true` if it is a full code. 396 */ 397 bool isFull() const @property nothrow @nogc 398 { 399 return _code.length > separatorPosition && _code[separatorPosition] == separator; 400 } 401 402 /** 403 * Returns whether this $(D OpenLocationCode) is a short Open Location Code. 404 * 405 * Returns: True if it is short. 406 */ 407 bool isShort() const @property nothrow @nogc 408 { 409 const index = _code.countUntil(separator); 410 return index >= 0 && index < separatorPosition; 411 } 412 413 /** 414 * Returns whether this $(D OpenLocationCode) is a padded Open Location Code, meaning that it 415 * contains less than 8 valid digits. 416 * 417 * @return True if this code is padded. 418 */ 419 bool isPadded() const @property nothrow @nogc 420 { 421 return _code.canFind(paddingCharacter); 422 } 423 424 /** 425 * Returns short $(D OpenLocationCode) from the full Open Location Code created by removing 426 * four or six digits, depending on the provided reference point. It removes as many digits as 427 * possible. 428 * 429 * Params: 430 * referenceLatitude = Reference point latitude degrees. 431 * referenceLongitude = Reference point longitude degrees. 432 * Returns: A short code if possible or OpenLocationCode.init if it is too far away. 433 * Throws: $(D OLCFormatException) if this is not called on a valid full code or called on a padded code. 434 */ 435 OpenLocationCode shorten(double referenceLatitude, double referenceLongitude) const 436 { 437 if (!isFull()) 438 throw new OLCFormatException("shorten() method could only be called on a full code."); 439 if (isPadded()) 440 throw new OLCFormatException("shorten() method can not be called on a padded code."); 441 return shortenTrustedFullNoPad(referenceLatitude, referenceLongitude); 442 } 443 444 /** 445 * Returns short $(D OpenLocationCode) from the full Open Location Code created by removing 446 * four or six digits, depending on the provided reference point. It removes as many digits as 447 * possible. Assumes this is a full code which isn't padded. 448 * 449 * Params: 450 * referenceLatitude = Reference point latitude degrees. 451 * referenceLongitude = Reference point longitude degrees. 452 * Returns: A short code if possible or OpenLocationCode.init if it is too far away. 453 */ 454 OpenLocationCode shortenTrustedFullNoPad(double referenceLatitude, double referenceLongitude) const nothrow @nogc 455 { 456 const codeArea = decodeTrustedFull(); 457 const range = max(abs(referenceLatitude - codeArea.centerLatitude), 458 abs(referenceLongitude - codeArea.centerLongitude)); 459 // We are going to check to see if we can remove three pairs, two pairs or just one pair of 460 // digits from the code. 461 for (int i = 4; i >= 1; i--) 462 { 463 // Check if we're close enough to shorten. The range must be less than 1/2 464 // the precision to shorten at all, and we want to allow some safety, so 465 // use 0.3 instead of 0.5 as a multiplier. 466 if (range < (computeLatitudePrecision(i * 2) * 0.3)) 467 { 468 // We're done. 469 return OpenLocationCode(_code[i * 2 .. $]); 470 } 471 } 472 return OpenLocationCode.init; 473 } 474 475 /** 476 * Recover the nearest match (if the code was a short code) representing a full Open 477 * Location Code from this (short) Open Location Code, given the reference location. 478 * 479 * Params: 480 * referenceLatitude = Reference point latitude degrees. 481 * referenceLongitude = Reference point longitude degrees. 482 * @return The nearest matching full code. 483 */ 484 OpenLocationCode recover(double referenceLatitude, double referenceLongitude) const 485 { 486 if (isFull) 487 { 488 // Note: each code is either full xor short, no other option. 489 return this; 490 } 491 referenceLatitude = clipLatitude(referenceLatitude); 492 referenceLongitude = normalizeLongitude(referenceLongitude); 493 494 const digitsToRecover = separatorPosition - _code.countUntil(separator); 495 // The precision (height and width) of the missing prefix in degrees. 496 const prefixPrecision = pow(cast(double) encodingBase, 2.0 - (digitsToRecover / 2)); 497 498 // Use the reference location to generate the prefix. 499 auto recoveredPrefix = OpenLocationCode.encode(referenceLatitude, 500 referenceLongitude)._code[0 .. digitsToRecover]; 501 // Combine the prefix with the short code and decode it. 502 const recovered = OpenLocationCode(recoveredPrefix ~ _code); 503 const recoveredCodeArea = recovered.decode(); 504 // Work out whether the new code area is too far from the reference location. If it is, we 505 // move it. It can only be out by a single precision step. 506 double recoveredLatitude = recoveredCodeArea.centerLatitude; 507 double recoveredLongitude = recoveredCodeArea.centerLongitude; 508 509 // Move the recovered latitude by one precision up or down if it is too far from the reference, 510 // unless doing so would lead to an invalid latitude. 511 const latitudeDiff = recoveredLatitude - referenceLatitude; 512 if (latitudeDiff > prefixPrecision / 2 && recoveredLatitude - prefixPrecision > -latitudeMax) 513 { 514 recoveredLatitude -= prefixPrecision; 515 } 516 else if (latitudeDiff < -prefixPrecision / 2 && recoveredLatitude + prefixPrecision < latitudeMax) 517 { 518 recoveredLatitude += prefixPrecision; 519 } 520 521 // Move the recovered longitude by one precision up or down if it is too far from the 522 // reference. 523 const longitudeDiff = recoveredCodeArea.centerLongitude - referenceLongitude; 524 if (longitudeDiff > prefixPrecision / 2) 525 { 526 recoveredLongitude -= prefixPrecision; 527 } 528 else if (longitudeDiff < -prefixPrecision / 2) 529 { 530 recoveredLongitude += prefixPrecision; 531 } 532 533 return OpenLocationCode.encode(recoveredLatitude, recoveredLongitude, 534 (cast(int) recovered.code.length) - 1); 535 } 536 537 /** 538 * Returns whether the bounding box specified by the Open Location Code contains provided point. 539 * 540 * Params: 541 * latitude = Latitude degrees. 542 * longitude = Longitude degrees. 543 * Returns: $(D true) if the coordinates are contained by the code. 544 * Throws: $(D OLCFormatException) if this is not a valid full code. 545 */ 546 bool contains(double latitude, double longitude) const @safe 547 { 548 return decode().contains(latitude, longitude); 549 } 550 551 /** 552 * Returns whether the bounding box specified by the Open Location Code contains provided point. 553 * 554 * Params: 555 * latitude = Latitude degrees. 556 * longitude = Longitude degrees. 557 * Returns: $(D true) if the coordinates are contained by the code. 558 */ 559 bool containsTrustedFull(double latitude, double longitude) const nothrow @nogc @safe 560 { 561 return decodeTrustedFull().contains(latitude, longitude); 562 } 563 564 string toString() const 565 { 566 return code; 567 } 568 } 569 570 /** 571 * Returns whether the provided code length 572 */ 573 bool isValidCodeLength(int codeLength) nothrow @nogc pure 574 { 575 const c = min(codeLength, maxDigitCount); 576 return c >= 4 && !(c < pairCodeLength && c % 2 == 1); 577 } 578 579 // Exposed static helper methods. 580 581 /** 582 * Returns whether the provided string is a valid Open Location code. 583 * 584 * Params: 585 * codeString = The code to check. 586 * Returns: True if it is a valid full or short code. 587 */ 588 bool isValidCode(string codeString) pure 589 { 590 return isValidUppercaseCode(codeString.toUpper); 591 } 592 593 /** 594 * Same as isValidCode but doesn't convert the code to uppercase before handling, thus failing validation when using lowercase characters. 595 */ 596 bool isValidUppercaseCode(string codeString) nothrow @nogc pure 597 { 598 if (codeString.length < 2) 599 return false; 600 601 const code = codeString.representation; 602 603 // there must be exactly one separator 604 const separatorPosition = code.countUntil(separator); 605 if (separatorPosition == -1 || code[separatorPosition + 1 .. $].canFind(separator)) 606 return false; 607 608 // there must be an even number of at most 8 characters before the separator 609 if (separatorPosition % 2 != 0 || separatorPosition > .separatorPosition) 610 return false; 611 612 // Check first two characters: only some values from the alphabet are permitted. 613 if (separatorPosition == .separatorPosition) 614 { 615 // First latitude character can only have first 9 values. 616 if (codeAlphabet.countUntil(code[0]) > 8) 617 return false; 618 619 // First longitude character can only have first 18 values. 620 if (codeAlphabet.countUntil(code[1]) > 17) 621 return false; 622 } 623 624 // Check the characters before the separator. 625 bool paddingStarted; 626 foreach (i, c; code[0 .. separatorPosition]) 627 { 628 if (!codeAlphabet.canFind(c) && c != paddingCharacter) 629 return false; // invalid character 630 631 if (paddingStarted) 632 { 633 // once padding starts, there must not be anything but padding. 634 if (c != paddingCharacter) 635 return false; 636 } 637 else if (c == paddingCharacter) 638 { 639 paddingStarted = true; 640 641 // short codes cannot have padding 642 if (separatorPosition < .separatorPosition) 643 return false; 644 645 // padding can start on even character: 2, 4 or 6. 646 if (!i.among!(2, 4, 6)) 647 return false; 648 } 649 } 650 651 if (code.length > separatorPosition + 1) 652 { 653 if (paddingStarted) 654 return false; 655 656 // only one character after separator is forbidden 657 const extra = code[separatorPosition + 1 .. $]; 658 if (extra.length == 1) 659 return false; 660 if (extra.any!(a => !codeAlphabet.canFind(a))) 661 return false; 662 } 663 664 return true; 665 } 666 667 /** 668 * Returns if the code is a valid full Open Location Code. 669 * 670 * Params: 671 * code = The code to check. 672 * Returns: True if it is a valid full code. 673 */ 674 bool isFullCode(string code) pure 675 { 676 return code.toUpper.isFullUppercaseCode; 677 } 678 679 /** 680 * Does the same as isFullCode, assuming the code is already uppercase, making this @nogc nothrow. 681 */ 682 bool isFullUppercaseCode(string code) nothrow @nogc pure 683 { 684 if (!code.isValidUppercaseCode) 685 return false; 686 return OpenLocationCode.fromTrustedString(code).isFull; 687 } 688 689 /** 690 * Returns if the code is a valid short Open Location Code. 691 * 692 * Params: 693 * code = The code to check. 694 * Returns: True if it is a valid short code. 695 */ 696 bool isShortCode(string code) pure 697 { 698 return code.toUpper.isShortUppercaseCode; 699 } 700 701 /** 702 * Does the same as isShortCode, assuming the code is already uppercase, making this @nogc nothrow. 703 */ 704 bool isShortUppercaseCode(string code) nothrow @nogc pure 705 { 706 if (!code.isValidUppercaseCode) 707 return false; 708 return OpenLocationCode.fromTrustedString(code).isShort; 709 } 710 711 // Private static methods. 712 713 private double clipLatitude(double latitude) nothrow @nogc pure 714 { 715 return min(max(latitude, -latitudeMax), latitudeMax); 716 } 717 718 private double normalizeLongitude(double longitude) nothrow @nogc pure 719 { 720 while (longitude < -longitudeMax) 721 { 722 longitude = longitude + longitudeMax * 2; 723 } 724 while (longitude >= longitudeMax) 725 { 726 longitude = longitude - longitudeMax * 2; 727 } 728 return longitude; 729 } 730 731 /** 732 * Compute the latitude precision value for a given code length. Lengths <= 10 have the same 733 * precision for latitude and longitude, but lengths > 10 have different precisions due to the 734 * grid method having fewer columns than rows. Copied from the JS implementation. 735 */ 736 private double computeLatitudePrecision(int codeLength) nothrow @nogc pure 737 { 738 if (codeLength <= codePrecisionNormal) 739 return pow(encodingBase, cast(double)(codeLength / -2 + 2)); 740 else 741 return pow(encodingBase, -3) / pow(gridRows, codeLength - pairCodeLength); 742 } 743 744 // ========== TESTS ========== 745 746 version (unittest) 747 { 748 import unit_threaded; 749 750 enum epsilon = 1e-10; 751 752 void epsilonTest(A, B)(A a, B b, in string file = __FILE__, in size_t line = __LINE__) 753 { 754 shouldApproxEqual(a, b, epsilon); 755 } 756 } 757 758 @("test validity") 759 @system unittest 760 { 761 import std.csv; 762 import std.stdio; 763 import std.typecons; 764 765 auto file = File("test_data/test_validity.csv", "r"); 766 foreach (record; file.byLine.filter!(a => !a.startsWith("#")).joiner("\n") 767 .csvReader!(Tuple!(string, "code", bool, "isValid", bool, "isShort", bool, "isFull"))) 768 { 769 void test() @safe 770 { 771 shouldEqual(isValidCode(record.code), record.isValid); 772 shouldEqual(isShortCode(record.code), record.isShort); 773 shouldEqual(isFullCode(record.code), record.isFull); 774 } 775 776 test(); 777 } 778 } 779 780 @("test shortening") 781 @system unittest 782 { 783 import std.csv; 784 import std.stdio; 785 import std.typecons; 786 787 auto file = File("test_data/test_short_codes.csv", "r"); 788 foreach (record; file.byLine.filter!(a => !a.startsWith("#")).joiner("\n") 789 .csvReader!(Tuple!(string, "code", double, "lat", double, "lng", string, 790 "shortCode", string, "testType"))) 791 { 792 void testShorten() @safe 793 { 794 auto olc = OpenLocationCode.fromString(record.code); 795 shouldEqual(olc.shorten(record.lat, record.lng).code, record.shortCode); 796 } 797 798 void testRecovery() @safe 799 { 800 auto olc = OpenLocationCode.fromString(record.shortCode); 801 shouldEqual(olc.recover(record.lat, record.lng).code, record.code); 802 } 803 804 if (record.testType == "B" || record.testType == "S") 805 testShorten(); 806 807 if (record.testType == "B" || record.testType == "R") 808 testRecovery(); 809 } 810 } 811 812 @("test encoding") 813 @system unittest 814 { 815 import std.csv; 816 import std.stdio; 817 import std.typecons; 818 819 auto file = File("test_data/test_encoding.csv", "r"); 820 foreach (record; file.byLine.filter!(a => !a.startsWith("#")).joiner("\n") 821 .csvReader!(Tuple!(double, "lat", double, "lng", int, "length", string, "code"))) 822 { 823 shouldEqual(OpenLocationCode.encode(record.lat, record.lng, record.length).code, record.code); 824 } 825 } 826 827 @("test decoding") 828 @system unittest 829 { 830 import std.csv; 831 import std.stdio; 832 import std.typecons; 833 834 auto file = File("test_data/test_decoding.csv", "r"); 835 foreach (record; file.byLine.filter!(a => !a.startsWith("#")).joiner("\n") 836 .csvReader!(Tuple!(string, "code", int, "length", double, "latLo", 837 double, "lngLo", double, "latHi", double, "lngHi"))) 838 { 839 auto olc = OpenLocationCode.fromString(record.code); 840 auto area = olc.decode(); 841 // test decode 842 shouldEqual(record.length, area.length); // Wrong length 843 epsilonTest(record.latLo, area.southLatitude); // Wrong low latitude 844 epsilonTest(record.latHi, area.northLatitude); // Wrong high latitude 845 epsilonTest(record.lngLo, area.westLongitude); // Wrong low longitude 846 epsilonTest(record.lngHi, area.eastLongitude); // Wrong high longitude 847 848 // test contains 849 shouldBeTrue(olc.contains(area.centerLatitude, area.centerLongitude)); // Containment relation is broken for the decoded middle point of code 850 shouldBeTrue(olc.contains(area.southLatitude, area.westLongitude)); // Containment relation is broken for the decoded bottom left corner 851 shouldBeFalse(olc.contains(area.northLatitude, area.eastLongitude)); // Containment relation is broken for the decoded top right corner 852 shouldBeFalse(olc.contains(area.southLatitude, area.eastLongitude)); // Containment relation is broken for the decoded bottom right corner 853 shouldBeFalse(olc.contains(area.northLatitude, area.westLongitude)); // Containment relation is broken for the decoded top left corner 854 } 855 } 856 857 @("test clipping") 858 unittest 859 { 860 shouldEqual(OpenLocationCode.encode(-90, 5), OpenLocationCode.encode(-91, 5)); 861 shouldEqual(OpenLocationCode.encode(90, 5), OpenLocationCode.encode(91, 5)); 862 shouldEqual(OpenLocationCode.encode(5, 175), OpenLocationCode.encode(5, -185)); 863 shouldEqual(OpenLocationCode.encode(5, 175), OpenLocationCode.encode(5, -905)); 864 shouldEqual(OpenLocationCode.encode(5, -175), OpenLocationCode.encode(5, 905)); 865 } 866 867 @("test max code length") 868 unittest 869 { 870 // Check that we do not return a code longer than is valid. 871 string code = OpenLocationCode.encode(51.3701125, -10.202665625, 1_000_000).code; 872 shouldEqual(maxDigitCount + 1, code.length); // Encoded code should have a length of maxDigitCount + 1 for the plus symbol 873 shouldBeTrue(isValidCode(code)); 874 // Extend the code with a valid character and make sure it is still valid. 875 string tooLongCode = code ~ "W"; 876 shouldBeTrue(isValidCode(tooLongCode)); // Too long code with all valid characters should be valid. 877 // Extend the code with an invalid character and make sure it is invalid. 878 tooLongCode = code ~ "U"; 879 shouldBeFalse(isValidCode(tooLongCode)); // Too long code with invalid character should be invalid. 880 } 881 882 @("test recovery near south pole") 883 unittest 884 { 885 shouldEqual(OpenLocationCode.fromString("XXXXXX+XX").recover(-81.0, 0.0).code, "2CXXXXXX+XX"); 886 } 887 888 @("test recovery near north pole") 889 unittest 890 { 891 shouldEqual(OpenLocationCode.fromString("2222+22").recover(89.6, 0.0).code, "CFX22222+22"); 892 } 893 894 @("test width in degrees") 895 unittest 896 { 897 epsilonTest(OpenLocationCode.fromString("67000000+").decode().longitudeWidth, 20.); 898 epsilonTest(OpenLocationCode.fromString("67890000+").decode().longitudeWidth, 1.); 899 epsilonTest(OpenLocationCode.fromString("6789CF00+").decode().longitudeWidth, 0.05); 900 epsilonTest(OpenLocationCode.fromString("6789CFGH+").decode().longitudeWidth, 0.0025); 901 epsilonTest(OpenLocationCode.fromString("6789CFGH+JM").decode().longitudeWidth, 0.000125); 902 epsilonTest(OpenLocationCode.fromString("6789CFGH+JMP").decode().longitudeWidth, 0.00003125); 903 } 904 905 @("test height in degrees") 906 unittest 907 { 908 epsilonTest(OpenLocationCode.fromString("67000000+").decode().latitudeHeight, 20.); 909 epsilonTest(OpenLocationCode.fromString("67890000+").decode().latitudeHeight, 1.); 910 epsilonTest(OpenLocationCode.fromString("6789CF00+").decode().latitudeHeight, 0.05); 911 epsilonTest(OpenLocationCode.fromString("6789CFGH+").decode().latitudeHeight, 0.0025); 912 epsilonTest(OpenLocationCode.fromString("6789CFGH+JM").decode().latitudeHeight, 0.000125); 913 epsilonTest(OpenLocationCode.fromString("6789CFGH+JMP").decode().latitudeHeight, 0.000025); 914 }