diff --git a/README.md b/README.md index 03347ef..5ff8890 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ and to decode mapcodes back to latitude/longitude pairs. If you wish to use mapcodes in your own application landscape, consider using running an instance of the Mapcode REST API, which can be found on: **https://github.com/mapcode-foundation/mapcode-rest-service** + # License Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,10 +38,81 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -Original C library created by Pieter Geelen. Work on Java version -of the mapcode library by Rijn Buve. Initial port by Matthew Lowden. -# Using the Mapcode Library +# Contents + +1. [What is a Mapcode?](#intro) +1. [Examples](#examples) +1. [Using the Mapcode Library](#library) + 1. [`MapcodeCodec`](#mapcodec) + 1. [`Mapcode`](#mapcode) + 1. [`Territory`](#territory) + 1. [`Alphabet`](#alphabet) + 1. [`Point`](#point) + 1. [`Rectangle`](#rectangle) +1. [Release Notes](#releasenotes) + + +# What Is A Mapcode? + +A mapcode represents a location. Every location on Earth can be represented by a mapcode. Mapcodes +were designed to be short, easy to recognise, remember and communicate. They are precise to a few +meters, which is good enough for every-day use. + + +## Mapcodes Are Free! + +Mapcodes are free. They can be used by anyone, and may be supported, provided or generated by anyone, +as long as this is done free of charge, conditions or restrictions. Technical details and sources are +available on our developers page. + + +## what Does A Mapcode Look Like? + +A mapcode consists of two groups of letters and digits, separated by a dot. An example of a mapcode is + + 49.4V + +This is sufficient as long as it is clear what country or state the mapcode belongs in. On a business card, +it is therefore a good idea to put it after the country or state name: + + John Smith + Oosterdoksstraat 114 + Amsterdam + Netherlands 49.4V + +When storing mapcodes in a database, it is recommended to explicitly specify the country: + + Netherlands 49.4V + +or via the standard 3-letter abbreviation: + + NLD 49.4V + +In eight very large countries (The USA, Canada, Mexico, Brazil, India, Australia, Russia, and China), +an address has little meaning without knowing the state (just like elsewhere, an address has little meaning +without knowing the country). For example, there are 27 cities called Washington in the USA. If you want to +refer to a location in the capital city, you would always refer to "Washington DC". + + DC 18.JQZ + +or (in an international database): + + US-DC 18.JQZ + +More information on mapcodes and their underlying concepts can be found in our reference material. + + +## Where Did Mapcodes Come From? + +Mapcodes were developed in 2001 by Pieter Geelen and Harold Goddijn, soon after the GPS satellite signals +were opened up for civilian use. It was decided to donate the mapcode system to the public domain in 2008. +The algorithms and data tables are maintained by the Stichting Mapcode Foundation. + +The mapcode system is being filed as a standard at the International Organisation for Standardisation. + + +# Examples For a description of what mapcodes are, please visit http://mapcode.com. @@ -53,22 +125,22 @@ library), such as Roman, Greek, Hindi and Arabic. `Mapcode` objects are returned by the `MapcodeCodec` with encodes coordinates to mapcodes and decodes mapcodes to coordinates (codec means coder/decoder). -# Examples - Here's an example to `decode()` mapcode within a given territory context. Note that the territory context (`NLD` is this case) is only used to disambiguate the code if needed. If the provided code is an international code, the context is simply ignored, because no disambiguation is needed. +``` final Territory territory = Territory.fromString("NLD"); final String mapcode = "49.4V"; try { final Point p = MapcodeCodec.decode(mapcode, territory); - // p now points at the (lat, lon) for the mapcode. + // p now contains the (lat, lon) for mapcode "49.YV". } catch (final UnknownMapcodeException ignored) { // The mapcode was not valid. } - +``` + And to `encode()` a latitude and longitude into a mapcode you would write: final List results = MapcodeCodec.encode(lat, lon); @@ -129,12 +201,225 @@ The `Point` class is usually easier to use than individual latitude and longitude paramters and class makes sure it always wraps latitudes to a range of `[-90, 90]` and longitude to `[-180, 180>`. -# Coding Formatting Style -The code formatting style used is the default code formatting style from IntelliJ IDEA (version 14). -it is recommended to always use auto-format on any (Java) files before committing to maintain consistent layout. +# Using the Mapcode Library + +Welcome to the Java library to handle mapcodes. The original C library was created by Pieter Geelen. +The initial port to Java and speed-ups were done by Matthew Lowden. +Rijn Buve has developed and contributed to the Java version of the Mapcode library since, providing +a simple and consistent API for other developers. He has also built a number of applications using +this library, which can also be found in the Github respositories of the Mapcode Foundation. + + +## How To Build The Mapcode Library JAR File + +The sources for the Mapcode Library for Java contain everything to build the Mapcode JAR file as well +as a significant number of unit tests to verify the correctness of the implementation against the +reference C implementation. + +The library requires a minimum Java language revision level 6, but has been tested and verified to work +with JDK 1.6, JDK 1.7 and JDK 1.8. + +To build the library: + + cd + mvn clean install + +This produces a JAR file in your local Maven repository at `~/.m2/repository/com/mapcode/mapcode//` +You can include this JAR in your project, or store it in your local Nexus repository, for example. + +If you create a Maven project, much simpler than building the library yourself, is to include it from +Maven Central, adding this dependency to your `pom.xml`: + + + com.mapcode + mapcode + {fill in latest version} + + +The latest official version of the libray on Maven Central can be found [**here**](http://search.maven.org/#search%7Cga%7C1%7Cmapcode). + +## How To Use This Library In Your Application + +There are two classes you interact with as a client of the Mapcode Library. These are: + + MapcodeCodec.java + Mapcode.java + Point.java + + +### Class `MapcodeCodec` + +This class contains the **encoder** and **decoder** for mapcodes. The class exposes two methods (with some +variants): `encode` and `decode`. + +The **encoder** encodes a (latitude, longitude) pair into a result set of mapcodes. A single (latitude, +longitude) pair may produce multiple mapcodes, some of which are known as **local** mapcodes +(which are only unique within a given territory) and one which is globally unique in the entire world. + +The **decoder** decodes a local or world-wide mapcode string into a (latitude, longitude) pair. For +local mapcodes a territory may be specified which is used to disambiguate the mapcode resolution. + +Note that encoding a (latitude, longitude) pair to a mapcode and then decoding it may result in a +slightly offset position, as the mapcodes have a limited precision. The library offers "high-precision" +mapcodes as well, but you van never assume the resulting latitudes and longitudes to exactly match the +original input. + +**`List encode(double latitude, double longitude)`** encodes a (latitude, longitude) pair. + +Example: + + double lat = 52.376514; + double lon = 4.908542; + List results = MapcodeCodec.encode(lat, lon); + // Returns a non-empty list of results. + +This produces a non-empty list of resulting mapcodes. The shortest (potentially local) mapcodes is always +the first mapcodes in the list. The last mapcode in the list is always the +globally unique international mapcode. + +**`List encode(double latitude, double longitude, Territory territory)`** encodes a (latitude, +longitude) pair, where encoding is restricted to a specific territory. + +Example: + + List results = MapcodeCodec.encode(lat, lon, Territory.NLD); + // Returns an empty list of results if the location is not within territory NLD. + +This resticts encoding to a specific territory and produces a potentially empty list of results. +Again, if encoding succeeded, the first mapcode is the shortest one and the last mapcode in the list is the +globally unique international mapcode. + +Both `encode()` methods are also offered as a `encodeToShortest()` method, which essentially +returns only the first result of the previous methods (if there are any results). + + Mapcode result = MapcodeCodec.encodeToShortest(lat, lon); + // Always returns a mapcode (or valid lat and lon values). -# Using Git and `.gitignore` + try { + Mapcode result = MapcodeCodec.encodeToShortest(lat, lon, Territory.NLD); + // This may fail. + } + catch (UnknownMapcodeException e) { + // If the location is not within the territory, this exception is thrown. + } + +**`Point decode(String mapcode)`** decodes a mapcode to a `Point` which contains a location. Example: + + Point p = MapcodeCodec.decode("NLD 49.4V"); + +**`Point decode(String mapcode, Territory territory)`** decodes a mapcode to a `Point` which contains a +location, where the mapcode must be located within a specific territory. + +Examples of usage: + + Point p = MapcodeCodec.decode("49.4V", Territory.NLD); + + +## Class `Mapcode` + +This class represents mapcodes, which consist of a string of characters, digits and a decimal point and +a territory specification. The territory specification is required for national (local) mapcodes, which +are not globally unique. + +The class also exposes methods to convert mapcodes to proper mapcode strings, usable for printing and +it allows string-formatted mapcodes to be converted to `Mapcode` objects, where territory information +is properly parsed and converted to a `Territory` enumeration value. + +**`String getCode()`** returns the mapcode string which does not include territory information. You can also +use `getCode(1)` and `getCode(2)` for more precision, but longer mapcodes. + +The default precision offered by `getCode()` is approximately 10m (maximum distance to latitude, +longitude the mapcode decodes to). This corresponds to an area of 20m x 20m (400m2). These mapcodes include +no additional precision digits. + +The precision offered by `getCode(1)` is approximately 2m. +This corresponds to an area of 4m x 4m (16m2). These mapcodes include 1 additional precision digit. + +The precision offered by `getCode(2)` is approximately 0.40m. This corresponds to an area +of 0.80m x 0.80m (0.64m2). These mapcodes include 2 additional precision digits. + +This goes up to `getCode(8)`, which provides nanometer accuracy. (Please note one of the main advantages +of mapcodes over WGS84 coordinates is their simplicity and short size, so try to use as little precision +as required for your application...) + +**`Territory getTerritory()`** returns the territory information. + +**`toString()`** and **`getCodeWithTerritory()`** return mapcodes string with territory information, +specified as a ISO code. + + +## Enum `Territory` + +This enum defines the territories for which local mapcodes are defined. The added benefit of using +local mapcodes over international mapcodes is simply that they are shorter and easier to remember. + +Rather than writing `WLR9B.RP9P` (to locate a park in Moscow) you can use `MOW HG.4L` (or `MOW НГ.4Л` +in Cyrillic). And most of the time you can even omit the prefix `MOW`, as in many practical situations +the territory is given implicitly by the context of usage. + +Note that in this case `MOW` is not really a territory, but a sub-territory of `RUS`. It's full +name is `RU-MOW`. + +The following territories are subdivided into subterritories to make sure the territory codes represent +smaller areas, so mapcodes can remain fairly short: + +* `USA`, `US-XXX`: USA +* `IND`, `IN-XXX`: India +* `CAN`, `CA-XXX`: Canada +* `AUS`, `AU-XXX`: Australia +* `MEX`, `MX-XXX`: Mexico +* `BRA`, `BR-XXX`: Brasil +* `RUS`, `RU-XXX`: Russia +* `CHN`, `CN-XXX`: China + +Rather than using the 3-letter territory code for mapcodes in these territories, you'd probably want +to use the `TT-XXX` form, where `XXX` defines the subterritory (state, province, etc.) + + +## Enum `Alphabet` + +This enum defines the alphabets, or rather scripts, that are supported by the Mapcode Library. +Encoding mapcodes procudes a Unicode string and this enum can be used to identify the script +for the result. + +Note that the character mapping between scripts is based on similarity in appearance, so mapcodes +in different scripts can be remembered more easily with the help of your visual memory. + + +## Class `Point` + +This class represents (latitude, longitude) locations. It offers methods to create locations using +degrees. + +**`Point fromDeg(double latitude, double longitude)`** returns a `Point` for a given (latitude, longitude) +pair. Note that latitudes are always between **-90** and **90**, and longitudes are +always between **-180** and **180** (non-inclusive) whenreturned. +However, values outside these range are correctly limited (latitude) or wrapped (longitude) to these ranges +when supplied to the class. + +The methods **`double getLat()`** and **`getLon()`** return the latitude and longitude respectively, in degrees. + + +## Class `Rectangle` + +This class represents a geospatial rectangle. This class only accurately represents areas on the +surface of the Earth for small rectangles (as the curvature of the surface is not taken into account). + +It is used to return the bounding box for a given mapcode. Beware: bounding boxes of mapcodes in a single +territory and of the same length do not overlap, but others may. The bounding boxes of international +mapcodes do not overlap with each other and they are smaller territorial mapcodes (but the codes are +longer). + + +## Code Style Settings for IntelliJ IDEA + +The Java code uses the *default* [JetBrains IntelliJ IDEA](https://www.jetbrains.com/idea) +code style settings for Java, with one exception: +code blocks are always surround by `{...}` and on separate lines. + + +## Using Git and `.gitignore` It's good practice to set up a personal global `.gitignore` file on your machine which filters a number of files on your file systems that you do not wish to submit to the Git repository. You can set up your own global @@ -160,7 +445,299 @@ The local `.gitignore` file in the Git repository itself to reflect those file o regular compile, build or release commands, such as: `target/ out/` -# Bug Reports and New Feature Requests + +## Bug Reports and New Feature Requests If you encounter any problems with this library, don't hesitate to use the `Issues` session to file your issues. Normally, one of our developers should be able to comment on them and fix. + + +# Release Notes + +These are the release notes for the Java library for mapcodes. + +### 2.4.4 + +* Added calls to decode an international or territorial mapcode to its encompassing +rectangle using `decodeToRectangle`. + +* Minor code hygiene improvements. + +* Moved all documentation to `README.md`. + +### 2.4.3 + +* Updated Maven dependencies for latest patches. + +### 2.4.2 + +* Removed secret Coveralls key from POM file. + +### 2.4.1 + +* Added scripts for Tifinagh (Berber), Tamil, Amharic, Telugu, Odia, Kannada, Gujarati. + +* Added `getAlphabets()` to `Territory` class, returning the most commonly used languages for the territory. + +* Renamed constant `HINDI` to `DEVANAGIRI`. + +* Improved some characters for Arabic and Devanagari. + +* Fixed Bengali to also support Assamese. + +### 2.4.0 + +* Added scripts for Korean (Choson'gul/Hangul), Burmese, Khmer, Sinhalese, Thaana (Maldivan), Chinese (Zhuyin, Bopomofo). + +* Renamed constant `MALAY` to `MALAYALAM`. + +### 2.3.1 + +* Fixed data for some parts of China. + +### 2.3.0 + +* Added Arabic support. + +* Fixed Greek, Hebrew and Hindi support. + +### 2.2.5 + +* Updated documentation. + +* Cleaned up POM, sorted dependencies. + +### 2.2.4 + +* Added Travis CI and Coveralls badges to `README.md`. + +* Replaces static `DataAccess` class with singleton `DataModel` to allow testing +of incorrect data model files. + +* Fixed error handling for incorrect data model files. + +* Fix error to info logging in `aeuUnpack`. + +* Updated all POM dependencies. + +* Updated copyright messages. + +* Improved test coverage of unit tests. + +### 2.2.3 + +* Issue #23: Fixed `Territory.fromString` to make sure the parent territory is valid for + input like "CHE-GR". This returned "MX-GRO" instead of throwing `UnknownTerritoryException`. + Added unit test for this type of case. + +* Fixed minor JavaDoc issues. + +### 2.2.2 + +* Fixed error in `Point` which in rare cases would allow longitudes outside proper range. + +### 2.2.1 + +* Fixed unit test. Reduced size of files for unit tests considerably. Improved unit test speed. + +* Fixed `Point` interface. + +* Cleaned up `Boundary` and `DataAccess`. + +### 2.2.0 + +* Solved 1-microdegree gap in a few spots on Earth, noticable now extreme precision is possible. + +* Replaced floating point by fixed point math. + +* Improved speed. + +* Enforce `Mencode(decode(M)) == M`, except at territory border corners. + +* Cleaned up source; moved hard-coded data into `mminfo.dat`. + +### 2.1.0 + +* Added micro-meter precision (mapcodes can now have eight precision digits). + +* Assure that encode(decode(m)) delivers m. + +* Renames to bring source more in line with other implementations. + +### 2.0.2 + +* Renamed `isValidPrecisionFormat` to `isValidMapcodeFormat`. + +* Removed public microdegree references from `Point` class. Everything is degrees now. + +* Removed `ParentTerritory` class. + +### 2.0.1 + +* Reverted Java JDK level to 1.6 (Java 6) from 1.8 (Java 8), so the library can be used on + Android platforms operating at Java 6 as well. + +* Use multi-threading for long running test to speed them up (uses all CPU cores now). + +* Added the ability to use a country name for `Territory.fromString()`. + +### 2.0.0 + +* Fixes to the data rectangles (primarily intended for ISO proposal). + +* Removed functionality to use numeric territory codes; only alpha codes are accepted. + +* Note that this release only allows high-precision mapcodes up to 2 additional suffix characters. +A future release will be scheduled to allow up to 8 suffix characters (nanometer accuracy). + +### 1.50.3 + +* This release breaks compatiblity with earlier releases, to clean up the interface significantly. + +* Removed `Mapcode.encodeToShortest(lat, lon))` as this will produce a randomly chosen territory. + You must specify a `restrictToTerritory` now. + +* Renamed `Territory.code` to `Territory.number`. + +* Renamed `fromTerritoryCode())` to `fromNumber())`. + +* Renamed `Territory.isState())` to `Territory.isSubdivision())` and + +* Renamed `Territory.hasStates())` to `Territory.hasSubdivision())`. + +* Renamed `Alphabet.code` to `Alphabet.number`. + +* Renamed `fromCode())` to `fromNumber())`. + +* Renamed `MapcodeFormat` to `PrecisionFormat`. + +* Deprecated methods have been removed. + +### 1.50.2 + +* Cleaned up Unicode handling a bit. + +* Speed up of reading initialization data. + +* Rename `toNameFormat` into `toAlphaFormat` and `NAME_FORMAT` to `ALPHA_FORMAT`. + +### 1.50.1 + +* Bugfix for mapcodes in IN-DD (in India). + +### 1.50 + +* Major release. This version is not backwards compatible with mapcode 1.4x: is has dropped support for + Antartica AT0-8 codes and has a changed (improved) way of dealing with the Greek alphabet. + +* Added 22-chararcter post-processing of all-digit mapcodes for the Greek alphabet. + +* Retired legacy aliases EAZ and SKM, AU-QL, AU-TS, AU-NI and AU-JB. + +* Retired legacy Antarctica claims AT0 through AT8. + +* Added convencience methods for `MapcodeCodec` to accept `Point` for all encode functions + as well (not just `latDeg`, `lonDeg`). + +* Added alphabet support to convert mapcodes (both codes and territories) between `Alphabet`s. + +* Exceptions have been corrected and documented in code. + +* Allowed nullable values in `MapcodeCodec` encode and decode methods to assume reasonable defaults. + +* Microdegrees are no longer support publicly in `Point`. Only degrees. + +* Latitudes are limited to -90..90 and longitudes are wrapped to -180..180 (non inclusive). + +### 1.42.3 + +* To be done. + +### 1.42.2 + +* Upper- and lowercase mapcodes always allowed. + +### 1.42.1 + +* Cleaned up source. Removed all pending IntelliJ IDEA inspection warnings and reformatted code + using default IDEA code style to maintain consistent layout. + +* Add additional unit tests to check for correct handling of international mapcode handling. + +* Added safe constants for the maximum delta distance in meters for mapcode accuracy. + +### 1.42 + +* Fixed a bug in `MapcodeCodec.encodeToShortest` which would not always return the shortest code (see + next bullet). Reproducible with `curl -X GET http://localhost:8080/mapcode/to/47.1243/-111.28564/local`. + +* Fixed a bug where `Encoder.encode` would sometime retrieve more than one result even if result set + was limited to 1 result. + +### 1.41.1 + +* Added convenience method to Mapcode. + +### 1.41 + +* Added the India state Telangana (IN-TG), until 2014 a region in Adhra Pradesh. + +* Updated POM dependencies to latest library versions of standard components. + +### 1.40.3 + +* Minor code clean-up with no functional effect. + +* (Issue #6) Removed non-project specific unwanted files out of `.gitignore`. These should be listed in the +developer's own global `~/.gitignore` file instead. + +### 1.40.2 + +* Added `getMapcodeFormatType` and `isValidMapcodeFormat` to check validity of mapcode strings. Added +unit tests for these methods as well. + +* Constructor of `Mapcode` now checks for validity of mapcode string. + +* Added Unicode handling of high precision mapcodes and added check to throw an `IllegalArgumentException` +if the character 'Z' or equivalent Unicode character is contained in the high precision part according to +the Mapcode documenation. + +* Added method `convertToAscii` which produces the ASCII, non-Unicode variant of a mapcode which contains +Unicode characters§. + +### 1.40.1 + +* Deprecated names `getMapcodeHighPrecision` and `getMapcodeMediumPrecision`. + Replaced those with `getMapcodePrecision1` and `getMapcodePrecision2`. + +* Fixed all occurences of incorrectly cased Mapcode vs. mapcode. + +### 1.40 + +* Renamed class `Mapcode` to `MapcodeCodec`. + +* Renamed class `MapcodeInfo` to `Mapcode`. + +* Added high precision Mapcodes, with methods `getMapcodeHighPrecision` + +* Seriously reduced test set size. + +* Replaced Unicode characters in source code to escapes. + +* Added explicit character encoding to `pom.xml`. + +* Fixed issues with decoder at some boundaries. + +### 1.33.2 + +* Clean-up of release 1.33.2. + +* Added release notes. + +* Removed GSON dependency from production (now scope 'test' only). + +* Added robustness with respect to Unicode characters. + +### 1.33.1 + +* First release of Java library for MapCodes. Includes extensive test suite. diff --git a/pom.xml b/pom.xml index 089bfb6..08b3281 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ mapcode jar - 2.4.3 + 2.4.4 Mapcode Java Library @@ -36,7 +36,7 @@ Rijn Buve Mapcode Foundation - Managing Director, development + Managing Director, main contributor diff --git a/src/main/java/com/mapcode/Boundary.java b/src/main/java/com/mapcode/Boundary.java index 73a1367..6fa3ab8 100644 --- a/src/main/java/com/mapcode/Boundary.java +++ b/src/main/java/com/mapcode/Boundary.java @@ -26,32 +26,28 @@ * This class handles territory rectangles for mapcodes. */ class Boundary { - private int lonMicroDegMin; // Minimum longitude (in microdegrees). Inclusive. - private int lonMicroDegMax; // Maximum longitude (in microdegrees). Exclusive. private int latMicroDegMin; // Minimum latitude (in microdegrees). Inclusive. + private int lonMicroDegMin; // Minimum longitude (in microdegrees). Inclusive. private int latMicroDegMax; // Minimum latitude (in microdegrees). Exclusive. + private int lonMicroDegMax; // Maximum longitude (in microdegrees). Exclusive. + // Get the singleton for the data model. private static final DataModel DATA_MODEL = DataModel.getInstance(); - private Boundary( - final int lonMicroDegMin, - final int lonMicroDegMax, - final int latMicroDegMin, - final int latMicroDegMax) { + private Boundary(final int latMicroDegMin, final int lonMicroDegMin, final int latMicroDegMax, final int lonMicroDegMax) { this.lonMicroDegMin = lonMicroDegMin; this.latMicroDegMin = latMicroDegMin; - this.lonMicroDegMax = lonMicroDegMax; this.latMicroDegMax = latMicroDegMax; + this.lonMicroDegMax = lonMicroDegMax; } // You have to use this factory method instead of a ctor. @Nonnull - static Boundary createFromTerritoryRecord(final int territoryRecord) { + static Boundary createBoundaryForTerritoryRecord(final int territoryRecord) { return new Boundary( - DATA_MODEL.getLonMicroDegMin(territoryRecord), - DATA_MODEL.getLonMicroDegMax(territoryRecord), - DATA_MODEL.getLatMicroDegMin(territoryRecord), - DATA_MODEL.getLatMicroDegMax(territoryRecord)); + DATA_MODEL.getLatMicroDegMin(territoryRecord), DATA_MODEL.getLonMicroDegMin(territoryRecord), + DATA_MODEL.getLatMicroDegMax(territoryRecord), DATA_MODEL.getLonMicroDegMax(territoryRecord) + ); } int getLonMicroDegMin() { @@ -98,7 +94,7 @@ boolean containsPoint(@Nonnull final Point p) { } final int lonMicroDeg = p.getLonMicroDeg(); - // Longitude boundaries can extend (slightly) outside the [-180,180) range + // Longitude boundaries can extend (slightly) outside the [-180,180) range. if (lonMicroDeg < lonMicroDegMin) { return (lonMicroDegMin <= (lonMicroDeg + Point.MICRO_DEG_360)) && ((lonMicroDeg + Point.MICRO_DEG_360) < lonMicroDegMax); } else if (lonMicroDeg >= lonMicroDegMax) { diff --git a/src/main/java/com/mapcode/CheckArgs.java b/src/main/java/com/mapcode/CheckArgs.java index 755bf0d..5bd9f6d 100644 --- a/src/main/java/com/mapcode/CheckArgs.java +++ b/src/main/java/com/mapcode/CheckArgs.java @@ -23,10 +23,10 @@ /** * ---------------------------------------------------------------------------------------------- - * Package private implementation class. For internal use within the Mapcode implementation only. + * Package private implementation class. For internal use within the mapcode implementation only. + * ---------------------------------------------------------------------------------------------- * * This class provides a number of helper methods to check (runtime) arguments. - * ---------------------------------------------------------------------------------------------- */ @SuppressWarnings("OverlyBroadThrowsClause") class CheckArgs { diff --git a/src/main/java/com/mapcode/Common.java b/src/main/java/com/mapcode/Common.java index 3485dec..270f6a5 100644 --- a/src/main/java/com/mapcode/Common.java +++ b/src/main/java/com/mapcode/Common.java @@ -29,16 +29,23 @@ class Common { private static final Logger LOG = LoggerFactory.getLogger(Common.class); - static final int[] nc = { + // TODO: Need better name and explanation. + static final int[] NC = { 1, 31, 961, 29791, 923521, 28629151, 887503681 }; - static final int[] xSide = { + + // TODO: Need better name and explanation. + static final int[] X_SIDE = { 0, 5, 31, 168, 961, 5208, 29791, 165869, 923521, 5141947 }; - static final int[] ySide = { + + // TODO: Need better name and explanation. + static final int[] Y_SIDE = { 0, 6, 31, 176, 961, 5456, 29791, 165869, 923521, 5141947 }; - private static final int[] xDivider19 = { + + // TODO: Need better name and explanation. + private static final int[] X_DIVIDER_19 = { 360, 360, 360, 360, 360, 360, 361, 361, 361, 361, 362, 362, 362, 363, 363, 363, 364, 364, 365, 366, 366, 367, 367, 368, 369, 370, 370, 371, 372, 373, @@ -66,6 +73,9 @@ private Common() { static { // This code shows a message when assertions are active or disabled. It (ab)uses assert for that... + // Some of the methods (and tests) take considerably longer with assertions checking, so it's useful + // to have this information in the log file. + //noinspection UnusedAssignment boolean debug = false; //noinspection AssertWithSideEffects @@ -73,59 +83,51 @@ private Common() { //noinspection ConstantConditions if (debug) { LOG.info("Common: assertions are active (JVM runtime option '-ea')"); - } - else { + } else { LOG.debug("Common: assertions are not active, they are bypassed"); } } - /** - * This method returns a divider for longitude (multiplied by 4), for a given latitude. - * - * @param minY Latitude. - * @param maxY Longitude. - * @return Divider. - */ - static int xDivider(final int minY, final int maxY) { - assert minY < maxY; - if (minY >= 0) { - // maxY > minY > 0 - assert (maxY > minY) && (minY > 0); - return xDivider19[minY >> 19]; - } else if (maxY >= 0) { - // maxY > 0 > minY - assert (maxY > 0) && (0 > minY); - return xDivider19[0]; + // This method returns a divider for longitude (multiplied by 4), for a given latitude. + // TODO: Need better names for minY and maxY. + static int xDivider(final int latMin, final int latMax) { + assert latMin < latMax; + if (latMin >= 0) { + assert (latMax > latMin) && (latMin > 0); + return X_DIVIDER_19[latMin >> 19]; + } else if (latMax >= 0) { + assert (latMax > 0) && (0 > latMin); + return X_DIVIDER_19[0]; } else { - // 0 > maxY > minY - assert (0 > maxY) && (maxY > minY); - return xDivider19[(-maxY) >> 19]; + assert (0 > latMax) && (latMax > latMin); + return X_DIVIDER_19[(-latMax) >> 19]; } } + // TODO: Need to explain what a codex is. static int countCityCoordinatesForCountry(final int codex, final int territoryRecord, final int firstTerritoryRecord) { assert codex >= 0; assert territoryRecord >= 0; assert firstTerritoryRecord >= 0; - final int i = getFirstNamelessRecord(codex, territoryRecord, firstTerritoryRecord); - int e = territoryRecord; - while (Data.getCodex(e) == codex) { - e++; + final int firstRecord = getFirstNamelessRecord(codex, territoryRecord, firstTerritoryRecord); + int record = territoryRecord; + while (Data.getCodex(record) == codex) { + record++; } - assert i <= e; - return e - i; + assert firstRecord <= record; + return record - firstRecord; } static int getFirstNamelessRecord(final int codex, final int territoryRecord, final int firstTerritoryRecord) { assert codex >= 0; assert territoryRecord >= 0; assert firstTerritoryRecord >= 0; - int i = territoryRecord; - while ((i >= firstTerritoryRecord) && Data.isNameless(i) && (Data.getCodex(i) == codex)) { - i--; + int record = territoryRecord; + while ((record >= firstTerritoryRecord) && Data.isNameless(record) && (Data.getCodex(record) == codex)) { + record--; } - i++; - assert i <= territoryRecord; - return i; + record++; + assert record <= territoryRecord; + return record; } } diff --git a/src/main/java/com/mapcode/Data.java b/src/main/java/com/mapcode/Data.java index 66c3c20..b389b0f 100644 --- a/src/main/java/com/mapcode/Data.java +++ b/src/main/java/com/mapcode/Data.java @@ -26,6 +26,8 @@ * This class the data class for Mapcode codex items. */ class Data { + + // TODO: Need explanation what this is and how this is used. static final char[] ENCODE_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // Numerals. 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', // Consonants. @@ -33,32 +35,40 @@ class Data { 'A', 'E', 'U' // Vowels. }; + // Get direct access to the data model. private static final DataModel DATA_MODEL = DataModel.getInstance(); private Data() { // Disabled. } + // TODO: Explain what these values are. Can we make this an enum instead (safer)? + static final int TERRITORY_RECORD_TYPE_NONE = 0; + static final int TERRITORY_RECORD_TYPE_PIPE = 1; + static final int TERRITORY_RECORD_TYPE_PLUS = 2; + static final int TERRITORY_RECORD_TYPE_STAR = 3; + + // TODO: Need to explain what "nameless" means and what a territoryRecord is (different from territoryNumber). static boolean isNameless(final int territoryRecord) { assert (0 <= territoryRecord) && (territoryRecord < DATA_MODEL.getNrTerritoryRecords()); return (DATA_MODEL.getDataFlags(territoryRecord) & 64) != 0; } + // TODO: Need to explain what "special shape" means. static boolean isSpecialShape(final int territoryRecord) { assert (0 <= territoryRecord) && (territoryRecord < DATA_MODEL.getNrTerritoryRecords()); + + // TODO: The "magic" of binary operators and bit shifting should be in class DataModel, not here. return (DATA_MODEL.getDataFlags(territoryRecord) & 1024) != 0; } - static final int TERRITORY_RECORD_TYPE_NONE = 0; - static final int TERRITORY_RECORD_TYPE_PIPE = 1; - static final int TERRITORY_RECORD_TYPE_PLUS = 2; - static final int TERRITORY_RECORD_TYPE_STAR = 3; - + // TODO: Explain what territory record types are. Can they be an enum instead? static int getTerritoryRecordType(final int territoryRecord) { assert (0 <= territoryRecord) && (territoryRecord < DATA_MODEL.getNrTerritoryRecords()); return (DATA_MODEL.getDataFlags(territoryRecord) >> 7) & 3; // 1=pipe 2=plus 3=star } + // TODO: Explain what "restricted" means. static boolean isRestricted(final int territoryRecord) { assert (0 <= territoryRecord) && (territoryRecord < DATA_MODEL.getNrTerritoryRecords()); return (DATA_MODEL.getDataFlags(territoryRecord) & 512) != 0; @@ -70,17 +80,15 @@ static int getCodex(final int territoryRecord) { return (10 * (codexflags / 5)) + (codexflags % 5) + 1; } + // TODO: What does this method do? What is parameter i (rename)? @Nonnull static String headerLetter(final int i) { final int flags = DATA_MODEL.getDataFlags(i); + + // TODO: The "magic" of how to interpret flags must be in DataModel, not here. if (((flags >> 7) & 3) == 1) { return Character.toString(ENCODE_CHARS[(flags >> 11) & 31]); } return ""; } - - @Nonnull - static Boundary getBoundary(final int territoryRecord) { - return Boundary.createFromTerritoryRecord(territoryRecord); - } } diff --git a/src/main/java/com/mapcode/DataModel.java b/src/main/java/com/mapcode/DataModel.java index a571657..14ec522 100644 --- a/src/main/java/com/mapcode/DataModel.java +++ b/src/main/java/com/mapcode/DataModel.java @@ -34,6 +34,7 @@ class DataModel { private static final Logger LOG = LoggerFactory.getLogger(DataModel.class); + // TODO: This class needs a thorough description of what the data file format looks like and what all bit fields means exactly. private static final int HEADER_ID_1 = 0; private static final int HEADER_ID_2 = 1; private static final int HEADER_VERSION_LO = 2; @@ -196,11 +197,13 @@ int getNrTerritories() { * * @return Number of rectangles per territory. */ + // TODO: Explain what territory records contain exactly. int getNrTerritoryRecords() { return nrTerritoryRecords; } @SuppressWarnings("PointlessArithmeticExpression") + // TODO: Explain what this does exactly, why not return a Point or Rectangle? int getLonMicroDegMin(final int territoryRecord) { return data[((territoryRecord * DATA_FIELDS_PER_REC) + POS_DATA_LON_MICRO_DEG_MIN)]; } @@ -221,17 +224,21 @@ int getDataFlags(final int territoryRecord) { return data[(territoryRecord * DATA_FIELDS_PER_REC) + POS_DATA_DATA_FLAGS] & MASK_DATA_DATA_FLAGS; } + // TODO: Explain what a "div" and "smart div" is and how you use, and why you need to use it. int getSmartDiv(final int territoryRecord) { return data[(territoryRecord * DATA_FIELDS_PER_REC) + POS_DATA_DATA_FLAGS] >> SHIFT_POS_DATA_SMART_DIV; } + // TODO: Explain what these methods do exactly. // Low-level routines for data access. @SuppressWarnings("PointlessArithmeticExpression") int getDataFirstRecord(final int territoryNumber) { + assert (0 <= territoryNumber) && (territoryNumber <= Territory.AAA.getNumber()); return index[territoryNumber + POS_INDEX_FIRST_RECORD]; } int getDataLastRecord(final int territoryNumber) { + assert (0 <= territoryNumber) && (territoryNumber <= Territory.AAA.getNumber()); return index[territoryNumber + POS_INDEX_LAST_RECORD] - 1; } } diff --git a/src/main/java/com/mapcode/Decoder.java b/src/main/java/com/mapcode/Decoder.java index e957487..96efff0 100644 --- a/src/main/java/com/mapcode/Decoder.java +++ b/src/main/java/com/mapcode/Decoder.java @@ -21,14 +21,20 @@ import javax.annotation.Nonnull; -import static com.mapcode.Boundary.createFromTerritoryRecord; +import static com.mapcode.Boundary.createBoundaryForTerritoryRecord; +/** + * ---------------------------------------------------------------------------------------------- + * Package private implementation class. For internal use within the Mapcode implementation only. + * ---------------------------------------------------------------------------------------------- + * + * This class contains decoder for mapcodes. + */ class Decoder { private static final Logger LOG = LoggerFactory.getLogger(Decoder.class); - private static final char GREEK_CAPITAL_ALPHA = '\u0391'; - - private static final DataModel dataModel = DataModel.getInstance(); + // Get direct access to the data model singleton. + private static final DataModel DATA_MODEL = DataModel.getInstance(); private Decoder() { // Prevent instantiation. @@ -39,90 +45,98 @@ private Decoder() { // ---------------------------------------------------------------------- @Nonnull - static Point decode(@Nonnull final String argMapcode, - @Nonnull final Territory argTerritory) + static MapcodeZone decodeToMapcodeZone(@Nonnull final String argMapcode, + @Nonnull final Territory argTerritory) throws UnknownMapcodeException { LOG.trace("decode: mapcode={}, territory={}", argMapcode, argTerritory.name()); String mapcode = argMapcode; Territory territory = argTerritory; - String extrapostfix = ""; - - final int minpos = mapcode.indexOf('-'); - if (minpos > 0) { - extrapostfix = decodeUTF16(mapcode.substring(minpos + 1).trim()); - if (extrapostfix.contains("Z")) { - throw new UnknownMapcodeException("Invalid character Z"); + String precisionPostfix = ""; + final int positionOfDash = mapcode.indexOf('-'); + if (positionOfDash > 0) { + precisionPostfix = decodeUTF16(mapcode.substring(positionOfDash + 1).trim()); + if (precisionPostfix.contains("Z")) { + throw new UnknownMapcodeException("Invalid character Z, mapcode=" + argMapcode + ", territory=" + argTerritory); } - mapcode = mapcode.substring(0, minpos); + + // Cut the precision postfix from the mapcode. + mapcode = mapcode.substring(0, positionOfDash); } + assert !mapcode.contains("-"); + // TODO: Explain what AEU unpack does. mapcode = aeuUnpack(mapcode).trim(); if (mapcode.isEmpty()) { - LOG.info("decode: Failed to aeuUnpack {}", argMapcode); - return Point.undefined(); // failed to aeuUnpack + // TODO: Is this a useful log message? + LOG.debug("decode: Failed to aeuUnpack {}", argMapcode); + throw new UnknownMapcodeException("Failed to AEU unpack, mapcode=" + argMapcode + ", territory=" + argTerritory); } - final int incodexlen = mapcode.length() - 1; + final int codexLen = mapcode.length() - 1; // *** long codes in states are handled by the country - if (incodexlen >= 9) { + if (codexLen >= 9) { + // International codes are 9 characters. + assert codexLen == 9; territory = Territory.AAA; } else { final Territory parentTerritory = territory.getParentTerritory(); - if (((incodexlen >= 8) && ((parentTerritory == Territory.USA) || (parentTerritory == Territory.CAN) - || (parentTerritory == Territory.AUS) || (parentTerritory == Territory.BRA) - || (parentTerritory == Territory.CHN) || (parentTerritory == Territory.RUS))) - || ((incodexlen >= 7) && - ((parentTerritory == Territory.IND) || (parentTerritory == Territory.MEX)))) { - + if (((codexLen >= 8) && + ((parentTerritory == Territory.USA) || (parentTerritory == Territory.CAN) || + (parentTerritory == Territory.AUS) || (parentTerritory == Territory.BRA) || + (parentTerritory == Territory.CHN) || (parentTerritory == Territory.RUS))) || + ((codexLen >= 7) && + ((parentTerritory == Territory.IND) || (parentTerritory == Territory.MEX)))) { territory = parentTerritory; } } + final int territoryNumber = territory.getNumber(); - final int ccode = territory.getNumber(); - - final int fromTerritoryRecord = dataModel.getDataFirstRecord(ccode); - final int uptoTerritoryRecord = dataModel.getDataLastRecord(ccode); - - final int incodexhi = mapcode.indexOf('.'); - final int incodex = (incodexhi * 10) + (incodexlen - incodexhi); + final int fromTerritoryRecord = DATA_MODEL.getDataFirstRecord(territoryNumber); + final int uptoTerritoryRecord = DATA_MODEL.getDataLastRecord(territoryNumber); - MapcodeZone mapcodeZone = MapcodeZone.empty(); + // Determine the codex pattern as 2-digits: length-of-left-part * 10 + length-of-right-part. + final int positionOfDot = mapcode.indexOf('.'); + final int codex = (positionOfDot * 10) + (codexLen - positionOfDot); + MapcodeZone mapcodeZone = new MapcodeZone(); for (int territoryRecord = fromTerritoryRecord; territoryRecord <= uptoTerritoryRecord; territoryRecord++) { - final int codexi = Data.getCodex(territoryRecord); - Boundary boundary = createFromTerritoryRecord(territoryRecord); + final int codexOfTerritory = Data.getCodex(territoryRecord); + final Boundary boundaryOfTerritory = createBoundaryForTerritoryRecord(territoryRecord); if (Data.getTerritoryRecordType(territoryRecord) == Data.TERRITORY_RECORD_TYPE_NONE) { + if (Data.isNameless(territoryRecord)) { // i = nameless - if (((codexi == 21) && (incodex == 22)) || - ((codexi == 22) && (incodex == 32)) || - ((codexi == 13) && (incodex == 23))) { - mapcodeZone = decodeNameless(mapcode, territoryRecord, extrapostfix); + if (((codexOfTerritory == 21) && (codex == 22)) || + ((codexOfTerritory == 22) && (codex == 32)) || + ((codexOfTerritory == 13) && (codex == 23))) { + mapcodeZone = decodeNameless(mapcode, territoryRecord, precisionPostfix); break; } } else { + // i = grid without headerletter - if ((codexi == incodex) || ((incodex == 22) && (codexi == 21))) { + if ((codexOfTerritory == codex) || + ((codex == 22) && (codexOfTerritory == 21))) { mapcodeZone = decodeGrid(mapcode, - boundary.getLonMicroDegMin(), boundary.getLatMicroDegMin(), - boundary.getLonMicroDegMax(), boundary.getLatMicroDegMax(), - territoryRecord, extrapostfix); + boundaryOfTerritory.getLonMicroDegMin(), boundaryOfTerritory.getLatMicroDegMin(), + boundaryOfTerritory.getLonMicroDegMax(), boundaryOfTerritory.getLatMicroDegMax(), + territoryRecord, precisionPostfix); // first of all, make sure the zone fits the country - mapcodeZone = mapcodeZone.restrictZoneTo(createFromTerritoryRecord(uptoTerritoryRecord)); + mapcodeZone = mapcodeZone.restrictZoneTo(createBoundaryForTerritoryRecord(uptoTerritoryRecord)); if (Data.isRestricted(territoryRecord) && !mapcodeZone.isEmpty()) { int nrZoneOverlaps = 0; int j; - Point result = mapcodeZone.getMidPoint(); + final Point result = mapcodeZone.getCenter(); // see if midpoint of mapcode zone is in any sub-area... for (j = territoryRecord - 1; j >= fromTerritoryRecord; j--) { if (!Data.isRestricted(j)) { - if (createFromTerritoryRecord(j).containsPoint(result)) { + if (createBoundaryForTerritoryRecord(j).containsPoint(result)) { nrZoneOverlaps++; break; } @@ -131,15 +145,15 @@ static Point decode(@Nonnull final String argMapcode, if (nrZoneOverlaps == 0) { // see if mapcode zone OVERLAPS any sub-area... - MapcodeZone zfound = MapcodeZone.empty(); + MapcodeZone zfound = new MapcodeZone(); for (j = fromTerritoryRecord; j < territoryRecord; j++) { // try all smaller rectangles j if (!Data.isRestricted(j)) { - MapcodeZone z = mapcodeZone.restrictZoneTo(createFromTerritoryRecord(j)); + final MapcodeZone z = mapcodeZone.restrictZoneTo(createBoundaryForTerritoryRecord(j)); if (!z.isEmpty()) { nrZoneOverlaps++; if (nrZoneOverlaps == 1) { // first fit! remember... - zfound.copyFrom(z); + zfound = new MapcodeZone(z); } else { // nrZoneOverlaps > 1 // more than one hit break; // give up! @@ -148,12 +162,12 @@ static Point decode(@Nonnull final String argMapcode, } } if (nrZoneOverlaps == 1) { // intersected exactly ONE sub-area? - mapcodeZone.copyFrom(zfound); // use the intersection found... + mapcodeZone = new MapcodeZone(zfound); // use the intersection found... } } if (nrZoneOverlaps == 0) { - mapcodeZone.setEmpty(); + mapcodeZone = new MapcodeZone(); } } break; @@ -161,30 +175,29 @@ static Point decode(@Nonnull final String argMapcode, } } else if (Data.getTerritoryRecordType(territoryRecord) == Data.TERRITORY_RECORD_TYPE_PIPE) { // i = grid with headerletter - if ((incodex == (codexi + 10)) && (Data.headerLetter(territoryRecord).charAt(0) == mapcode.charAt(0))) { + if ((codex == (codexOfTerritory + 10)) && + (Data.headerLetter(territoryRecord).charAt(0) == mapcode.charAt(0))) { mapcodeZone = decodeGrid(mapcode.substring(1), - boundary.getLonMicroDegMin(), boundary.getLatMicroDegMin(), - boundary.getLonMicroDegMax(), boundary.getLatMicroDegMax(), - territoryRecord, extrapostfix); + boundaryOfTerritory.getLonMicroDegMin(), boundaryOfTerritory.getLatMicroDegMin(), + boundaryOfTerritory.getLonMicroDegMax(), boundaryOfTerritory.getLatMicroDegMax(), + territoryRecord, precisionPostfix); break; } } else { + assert (Data.getTerritoryRecordType(territoryRecord) == Data.TERRITORY_RECORD_TYPE_PLUS) || + (Data.getTerritoryRecordType(territoryRecord) == Data.TERRITORY_RECORD_TYPE_STAR); // i = autoheader - if (((incodex == 23) && (codexi == 22)) || ((incodex == 33) && (codexi == 23))) { - mapcodeZone = decodeAutoHeader(mapcode, territoryRecord, extrapostfix); + if (((codex == 23) && (codexOfTerritory == 22)) || + ((codex == 33) && (codexOfTerritory == 23))) { + mapcodeZone = decodeAutoHeader(mapcode, territoryRecord, precisionPostfix); break; } } } - mapcodeZone = mapcodeZone.restrictZoneTo(createFromTerritoryRecord(uptoTerritoryRecord)); - - final Point result = mapcodeZone.getMidPoint(); - - LOG.trace("decode: result=({}, {})", - result.isDefined() ? result.getLatDeg() : Double.NaN, - result.isDefined() ? result.getLonDeg() : Double.NaN); - return result; + mapcodeZone = mapcodeZone.restrictZoneTo(createBoundaryForTerritoryRecord(uptoTerritoryRecord)); + LOG.trace("decode: zone={}", mapcodeZone); + return mapcodeZone; } // ---------------------------------------------------------------------- @@ -224,6 +237,9 @@ public Unicode2Ascii(final char min, final char max, @Nonnull final String conve } } + // Greek character A. + private static final char GREEK_CAPITAL_ALPHA = '\u0391'; + // Special character '?' indicating missing character in alphabet. private static final char MISSCODE = '?'; @@ -308,8 +324,14 @@ public Unicode2Ascii(final char min, final char max, @Nonnull final String conve }; @Nonnull - private static MapcodeZone decodeGrid(final String str, final int minx, final int miny, final int maxx, final int maxy, - final int m, final String extrapostfix) { + private static MapcodeZone decodeGrid( + @Nonnull final String str, + final int minx, + final int miny, + final int maxx, + final int maxy, + final int m, + @Nonnull final String extrapostfix) { // for a well-formed result, and integer variables String result = str; int relx; @@ -325,12 +347,12 @@ private static MapcodeZone decodeGrid(final String str, final int minx, final in final int divx; int divy; - divy = dataModel.getSmartDiv(m); + divy = DATA_MODEL.getSmartDiv(m); if (divy == 1) { - divx = Common.xSide[prelen]; - divy = Common.ySide[prelen]; + divx = Common.X_SIDE[prelen]; + divy = Common.Y_SIDE[prelen]; } else { - divx = Common.nc[prelen] / divy; + divx = Common.NC[prelen] / divy; } if ((prelen == 4) && (divx == 961) && (divy == 961)) { @@ -355,9 +377,9 @@ private static MapcodeZone decodeGrid(final String str, final int minx, final in rely = miny + (rely * ygridsize); relx = minx + (relx * xgridsize); - final int yp = Common.ySide[postlen]; + final int yp = Common.Y_SIDE[postlen]; final int dividery = ((ygridsize + yp) - 1) / yp; - final int xp = Common.xSide[postlen]; + final int xp = Common.X_SIDE[postlen]; final int dividerx = ((xgridsize + xp) - 1) / xp; String rest = result.substring(prelen + 1); @@ -384,10 +406,10 @@ private static MapcodeZone decodeGrid(final String str, final int minx, final in final int cornery = rely + (dify * dividery); final int cornerx = relx + (difx * dividerx); - Point pt = Point.fromMicroDeg(cornery, cornerx); - if (!(createFromTerritoryRecord(m).containsPoint(pt))) { - LOG.info("decodeGrid: Failed decodeGrid({}): {} not in {}", str, pt, createFromTerritoryRecord(m)); - return MapcodeZone.empty(); // already out of range + final Point pt = Point.fromMicroDeg(cornery, cornerx); + if (!(createBoundaryForTerritoryRecord(m).containsPoint(pt))) { + LOG.info("decodeGrid: Failed decodeGrid({}): {} not in {}", str, pt, createBoundaryForTerritoryRecord(m)); + return new MapcodeZone(); // already out of range } final int decodeMaxx = ((relx + xgridsize) < maxx) ? (relx + xgridsize) : maxx; @@ -397,7 +419,10 @@ private static MapcodeZone decodeGrid(final String str, final int minx, final in } @Nonnull - private static MapcodeZone decodeNameless(final String str, final int firstrec, final String extrapostfix) { + private static MapcodeZone decodeNameless( + @Nonnull final String str, + final int firstrec, + @Nonnull final String extrapostfix) { String result = str; final int codexm = Data.getCodex(firstrec); if (codexm == 22) { @@ -406,7 +431,7 @@ private static MapcodeZone decodeNameless(final String str, final int firstrec, result = result.substring(0, 2) + result.substring(3); } - int a = Common.countCityCoordinatesForCountry(codexm, firstrec, firstrec); + final int a = Common.countCityCoordinatesForCountry(codexm, firstrec, firstrec); final int p = 31 / a; final int r = 31 % a; @@ -464,15 +489,15 @@ private static MapcodeZone decodeNameless(final String str, final int firstrec, } if (nrX > a) { // past end! - return MapcodeZone.empty(); + return new MapcodeZone(); } final int territoryRecord = firstrec + nrX; - int side = dataModel.getSmartDiv(territoryRecord); + int side = DATA_MODEL.getSmartDiv(territoryRecord); int xSIDE = side; - Boundary boundary = createFromTerritoryRecord(territoryRecord); + final Boundary boundary = createBoundaryForTerritoryRecord(territoryRecord); final int maxx = boundary.getLonMicroDegMax(); final int maxy = boundary.getLatMicroDegMax(); final int minx = boundary.getLonMicroDegMin(); @@ -497,7 +522,7 @@ private static MapcodeZone decodeNameless(final String str, final int firstrec, if (dx >= xSIDE) // else out-of-range! { LOG.error("decodeGrid: Failed, decodeNameless({}): dx {} > xSIDE {}", str, dx, xSIDE); - return MapcodeZone.empty(); // return undefined (out of range!) + return new MapcodeZone(); // return undefined (out of range!) } final int dividerx4 = Common.xDivider(miny, maxy); // 4 times too large! @@ -510,7 +535,10 @@ private static MapcodeZone decodeNameless(final String str, final int firstrec, } @Nonnull - private static MapcodeZone decodeAutoHeader(final String input, final int m, final String extrapostfix) { + private static MapcodeZone decodeAutoHeader( + final String input, + final int m, + @Nonnull final String extrapostfix) { // returns Point.isUndefined() in case or error int storageStart = 0; final int codexm = Data.getCodex(m); @@ -525,13 +553,13 @@ private static MapcodeZone decodeAutoHeader(final String input, final int m, fin while (true) { if ((Data.getTerritoryRecordType(i) < Data.TERRITORY_RECORD_TYPE_PLUS) || (Data.getCodex(i) != codexm)) { LOG.error("decodeGrid: Failed, decodeAutoHeader({}): out of {} records", input, codexm); - return MapcodeZone.empty(); // return undefined + return new MapcodeZone(); // return undefined } - final int maxx = createFromTerritoryRecord(i).getLonMicroDegMax(); - final int maxy = createFromTerritoryRecord(i).getLatMicroDegMax(); - final int minx = createFromTerritoryRecord(i).getLonMicroDegMin(); - final int miny = createFromTerritoryRecord(i).getLatMicroDegMin(); + final int maxx = createBoundaryForTerritoryRecord(i).getLonMicroDegMax(); + final int maxy = createBoundaryForTerritoryRecord(i).getLatMicroDegMax(); + final int minx = createBoundaryForTerritoryRecord(i).getLonMicroDegMin(); + final int miny = createBoundaryForTerritoryRecord(i).getLatMicroDegMin(); int h = ((maxy - miny) + 89) / 90; final int xdiv = Common.xDivider(miny, maxy); @@ -564,7 +592,7 @@ private static MapcodeZone decodeAutoHeader(final String input, final int m, fin if ((cornerx < minx) || (cornerx >= maxx) || (cornery < miny) || (cornery > maxy)) { LOG.error("decodeGrid: Failed, decodeAutoHeader({}): corner {}, {} out of bounds", input, cornery, cornerx); - return MapcodeZone.empty(); // corner out of bounds + return new MapcodeZone(); // corner out of bounds } return decodeExtension(cornery, cornerx, dividerx << 2, -dividery, extrapostfix, @@ -576,7 +604,7 @@ private static MapcodeZone decodeAutoHeader(final String input, final int m, fin } @Nonnull - private static String aeuUnpack(final String argStr) { + private static String aeuUnpack(@Nonnull final String argStr) { // unpack encoded into all-digit // (assume str already uppercase!), returns "" in case of error String str = decodeUTF16(argStr); @@ -663,7 +691,8 @@ private static String aeuUnpack(final String argStr) { * @param mapcode Unicode string. * @return ASCII string. */ - static String decodeUTF16(final String mapcode) { + @Nonnull + static String decodeUTF16(@Nonnull final String mapcode) { String result; final StringBuilder asciiBuf = new StringBuilder(); for (final char ch : mapcode.toCharArray()) { @@ -706,7 +735,10 @@ static String decodeUTF16(final String mapcode) { } } - static String encodeUTF16(final String mapcodeInput, final int alphabetCode) throws IllegalArgumentException { + @Nonnull + static String encodeUTF16( + @Nonnull final String mapcodeInput, + final int alphabetCode) throws IllegalArgumentException { final String mapcode; if ((alphabetCode == Alphabet.GREEK.getNumber()) || @@ -747,7 +779,7 @@ static String encodeUTF16(final String mapcodeInput, final int alphabetCode) thr } @Nonnull - private static Point decodeTriple(final String str) { + private static Point decodeTriple(@Nonnull final String str) { final int c1 = DECODE_CHARS[(int) str.charAt(0)]; final int x = decodeBase31(str.substring(1)); if (c1 < 24) { @@ -757,8 +789,11 @@ private static Point decodeTriple(final String str) { } @Nonnull - private static Point decodeSixWide(final int v, final int width, final int height) { - int d; + private static Point decodeSixWide( + final int v, + final int width, + final int height) { + final int d; int col = v / (height * 6); final int maxcol = (width - 4) / 6; if (col >= maxcol) { @@ -774,7 +809,7 @@ private static Point decodeSixWide(final int v, final int width, final int heigh // / lowest level encode/decode routines // decode up to dot or EOS; // returns negative in case of error - private static int decodeBase31(final String code) { + private static int decodeBase31(@Nonnull final String code) { int value = 0; for (final char c : code.toCharArray()) { if (c == '.') { @@ -789,9 +824,16 @@ private static int decodeBase31(final String code) { } @Nonnull - private static MapcodeZone decodeExtension(final int y, final int x, final int dividerx0, final int dividery0, - final String extrapostfix, final int lon_offset4, final int extremeLatMicroDeg, final int maxLonMicroDeg) { - MapcodeZone mapcodeZone = new MapcodeZone(); + private static MapcodeZone decodeExtension( + final int y, + final int x, + final int dividerx0, + final int dividery0, + @Nonnull final String extrapostfix, + final int lon_offset4, + final int extremeLatMicroDeg, + final int maxLonMicroDeg) { + final MapcodeZone mapcodeZone = new MapcodeZone(); double dividerx4 = (double) dividerx0; double dividery = (double) dividery0; double processor = 1; @@ -807,7 +849,7 @@ private static MapcodeZone decodeExtension(final int y, final int x, final int d c1 = DECODE_CHARS[c1]; if ((c1 < 0) || (c1 == 30)) { LOG.error("decodeGrid; Failed, decodeExtension({}): illegal c1 {}", extrapostfix, c1); - return MapcodeZone.empty(); + return new MapcodeZone(); } final int y1 = c1 / 5; final int x1 = c1 % 5; @@ -819,7 +861,7 @@ private static MapcodeZone decodeExtension(final int y, final int x, final int d c2 = DECODE_CHARS[c2]; if ((c2 < 0) || (c2 == 30)) { LOG.error("decodeGrid: Failed, decodeExtension({}): illegal c2 {}", extrapostfix, c2); - return MapcodeZone.empty(); + return new MapcodeZone(); } y2 = c2 / 6; x2 = c2 % 6; @@ -840,8 +882,8 @@ private static MapcodeZone decodeExtension(final int y, final int x, final int d processor *= 30; } - double lon4 = (x * 4 * Point.MAX_PRECISION_FACTOR) + (lon32 * dividerx4) + (lon_offset4 * Point.MAX_PRECISION_FACTOR); - double lat1 = (y * Point.MAX_PRECISION_FACTOR) + (lat32 * dividery); + final double lon4 = (x * 4 * Point.MAX_PRECISION_FACTOR) + (lon32 * dividerx4) + (lon_offset4 * Point.MAX_PRECISION_FACTOR); + final double lat1 = (y * Point.MAX_PRECISION_FACTOR) + (lat32 * dividery); // determine the range of coordinates that are encode to this mapcode if (odd) { // odd @@ -866,9 +908,9 @@ private static MapcodeZone decodeExtension(final int y, final int x, final int d return mapcodeZone; } - private static boolean isAbjadScript(final String argStr) { + private static boolean isAbjadScript(@Nonnull final String argStr) { for (final char ch : argStr.toCharArray()) { - int c = (int) ch; + final int c = (int) ch; if ((c >= 0x0628) && (c <= 0x0649)) { return true; // Arabic } @@ -878,14 +920,15 @@ private static boolean isAbjadScript(final String argStr) { if ((c >= 0x388) && (c <= 0x3C9)) { return true; // Greek uppercase and lowecase } - if ((c >= 0x1100) && (c <= 0x1174) || (c >= 0xad6c) && (c <= 0xd314)) { + if (((c >= 0x1100) && (c <= 0x1174)) || ((c >= 0xad6c) && (c <= 0xd314))) { return true; // Korean } } return false; } - private static String convertFromAbjad(final String mapcode) { + @Nonnull + private static String convertFromAbjad(@Nonnull final String mapcode) { // split into prefix, s, postfix int p = mapcode.lastIndexOf(' '); if (p < 0) { @@ -894,7 +937,7 @@ private static String convertFromAbjad(final String mapcode) { p++; } final String prefix = mapcode.substring(0, p); - String remainder = mapcode.substring(p); + final String remainder = mapcode.substring(p); final String postfix; final int h = remainder.indexOf('-'); final String s; @@ -961,7 +1004,8 @@ private static String convertFromAbjad(final String mapcode) { } @SuppressWarnings("NumericCastThatLosesPrecision") - private static String convertToAbjad(final String mapcode) { + @Nonnull + private static String convertToAbjad(@Nonnull final String mapcode) { String str; final String rest; final int h = mapcode.indexOf('-'); diff --git a/src/main/java/com/mapcode/Encoder.java b/src/main/java/com/mapcode/Encoder.java index e3522be..8e9638a 100644 --- a/src/main/java/com/mapcode/Encoder.java +++ b/src/main/java/com/mapcode/Encoder.java @@ -24,13 +24,21 @@ import java.util.ArrayList; import java.util.List; -import static com.mapcode.Boundary.createFromTerritoryRecord; +import static com.mapcode.Boundary.createBoundaryForTerritoryRecord; import static com.mapcode.Common.*; +/** + * ---------------------------------------------------------------------------------------------- + * Package private implementation class. For internal use within the Mapcode implementation only. + * ---------------------------------------------------------------------------------------------- + * + * This class contains encoder for mapcodes. + */ class Encoder { private static final Logger LOG = LoggerFactory.getLogger(Encoder.class); - private static final DataModel dataModel = DataModel.getInstance(); + // Get direct access to data model singleton. + private static final DataModel DATA_MODEL = DataModel.getInstance(); private Encoder() { // Prevent instantiation. @@ -45,9 +53,9 @@ static List encode( final double latDeg, final double lonDeg, @Nullable final Territory territory, - final boolean stopWithOneResult) { + final boolean limitToOneResult) { - return encode(latDeg, lonDeg, territory, stopWithOneResult, null); + return encode(latDeg, lonDeg, territory, limitToOneResult, null); } // ---------------------------------------------------------------------- @@ -58,68 +66,88 @@ static List encode( 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'A', 'E', 'U'}; @Nonnull - private static List encode(final double argLatDeg, final double argLonDeg, - @Nullable final Territory territory, final boolean limitToOneResult, - @Nullable final Territory argStateOverride) { + private static List encode( + final double argLatDeg, + final double argLonDeg, + @Nullable final Territory territory, + final boolean limitToOneResult, + @Nullable final Territory argStateOverride) { LOG.trace("encode: latDeg={}, lonDeg={}, territory={}, limitToOneResult={}", argLatDeg, argLonDeg, (territory == null) ? null : territory.name(), limitToOneResult); final Point pointToEncode = Point.fromDeg(argLatDeg, argLonDeg); - final List results = new ArrayList(); + int lastBaseSubTerritoryNumber = -1; - int lastbasesubareaID = -1; + // Determine whether to walk through all records, or just for one (given) territory. + final int firstTerritoryRecord = (territory != null) ? territory.getNumber() : 0; + final int lastTerritoryRecord = (territory != null) ? territory.getNumber() : Territory.AAA.getNumber(); + for (int territoryRecord = firstTerritoryRecord; territoryRecord <= lastTerritoryRecord; territoryRecord++) { - final int firstNr = (territory != null) ? territory.getNumber() : 0; - final int lastNr = (territory != null) ? territory.getNumber() : Territory.AAA.getNumber(); - for (int ccode = firstNr; ccode <= lastNr; ccode++) { + // Check if the point to encode is covered by the last data record. + final int firstSubTerritoryRecord = DATA_MODEL.getDataLastRecord(territoryRecord); + if (createBoundaryForTerritoryRecord(firstSubTerritoryRecord).containsPoint(pointToEncode)) { - final int uptoTerritoryRecord = dataModel.getDataLastRecord(ccode); - if (!createFromTerritoryRecord(uptoTerritoryRecord).containsPoint(pointToEncode)) { - continue; - } - final int fromTerritoryRecord = dataModel.getDataFirstRecord(ccode); - final Territory currentEncodeTerritory = Territory.fromNumber(ccode); - - for (int territoryRecord = fromTerritoryRecord; territoryRecord <= uptoTerritoryRecord; territoryRecord++) { - if (createFromTerritoryRecord(territoryRecord).containsPoint(pointToEncode)) { - String mapcode = ""; - if (Data.isNameless(territoryRecord)) { - mapcode = encodeNameless(pointToEncode, territoryRecord, fromTerritoryRecord); - } else if (Data.getTerritoryRecordType(territoryRecord) > Data.TERRITORY_RECORD_TYPE_PIPE) { - mapcode = encodeAutoHeader(pointToEncode, territoryRecord); - } else if ((territoryRecord == uptoTerritoryRecord) && (currentEncodeTerritory.getParentTerritory() != null)) { - results.addAll(encode(argLatDeg, argLonDeg, currentEncodeTerritory.getParentTerritory(), limitToOneResult, - currentEncodeTerritory)); - continue; - } else if (!Data.isRestricted(territoryRecord) || (lastbasesubareaID == fromTerritoryRecord)) { - if (Data.getCodex(territoryRecord) < 54) { - mapcode = encodeGrid(territoryRecord, pointToEncode); - } - } + final int lastSubTerritoryRecord = DATA_MODEL.getDataFirstRecord(territoryRecord); + final Territory currentEncodeTerritory = Territory.fromNumber(territoryRecord); - if (mapcode.length() > 4) { - mapcode = aeuPack(mapcode, false); + for (int subTerritoryRecord = lastSubTerritoryRecord; subTerritoryRecord <= firstSubTerritoryRecord; subTerritoryRecord++) { - final Territory encodeTerritory = (argStateOverride != null) ? argStateOverride : currentEncodeTerritory; + // Check if the point to encode is contained within the boundary. + if (createBoundaryForTerritoryRecord(subTerritoryRecord).containsPoint(pointToEncode)) { - // Create new result. - final Mapcode newResult = new Mapcode(mapcode, encodeTerritory); + // All fine, proceed with creating a mapcode. + String mapcode = ""; + if (Data.isNameless(subTerritoryRecord)) { + mapcode = encodeNameless(pointToEncode, subTerritoryRecord, lastSubTerritoryRecord); - // The result should not be stored yet. - if (!results.contains(newResult)) { - if (limitToOneResult) { - results.clear(); + } else if (Data.getTerritoryRecordType(subTerritoryRecord) > Data.TERRITORY_RECORD_TYPE_PIPE) { + mapcode = encodeAutoHeader(pointToEncode, subTerritoryRecord); + + } else if ((subTerritoryRecord == firstSubTerritoryRecord) && + (currentEncodeTerritory.getParentTerritory() != null)) { + results.addAll(encode(argLatDeg, argLonDeg, currentEncodeTerritory.getParentTerritory(), + limitToOneResult, currentEncodeTerritory)); + continue; + + } else if (!Data.isRestricted(subTerritoryRecord) || (lastBaseSubTerritoryNumber == lastSubTerritoryRecord)) { + if (Data.getCodex(subTerritoryRecord) < 54) { + mapcode = encodeGrid(subTerritoryRecord, pointToEncode); } - results.add(newResult); } else { - LOG.error("encode: Duplicate results found, newResult={}, results={} items", - newResult.getCodeWithTerritory(), results.size()); + // Skip this record. } - lastbasesubareaID = fromTerritoryRecord; - if (limitToOneResult) { - return results; + // Check if we created a mapcode. + if (!mapcode.isEmpty()) { + assert mapcode.length() > 4; + mapcode = aeuPack(mapcode, false); + + final Territory encodeTerritory = (argStateOverride != null) ? + argStateOverride : currentEncodeTerritory; + + // Create new result. + final Mapcode newResult = new Mapcode(mapcode, encodeTerritory); + + // The result should not be stored yet. + if (!results.contains(newResult)) { + + // Remove existing results (if there was a parent territory). + if (limitToOneResult) { + results.clear(); + } + results.add(newResult); + } else { + LOG.error("encode: Duplicate results found, newResult={}, results={} items", + newResult.getCodeWithTerritory(), results.size()); + } + + lastBaseSubTerritoryNumber = lastSubTerritoryRecord; + + // Stop if we only need a single result anyway. + if (limitToOneResult) { + return results; + } } } } @@ -130,7 +158,14 @@ private static List encode(final double argLatDeg, final double argLonD return results; } - private static String encodeExtension(final Point pointToEncode, final int extrax4, final int extray, final int dividerx4, final int dividery, final int ydirection) { + @Nonnull + private static String encodeExtension( + final Point pointToEncode, + final int extrax4, + final int extray, + final int dividerx4, + final int dividery, + final int ydirection) { int extraDigits = 8; // always generate 8 digits double factorx = Point.MAX_PRECISION_FACTOR * dividerx4; @@ -168,8 +203,11 @@ private static String encodeExtension(final Point pointToEncode, final int extra return sb.toString(); } - private static String encodeGrid(final int m, final Point pointToEncode) { - int codexm = Data.getCodex(m); + @Nonnull + private static String encodeGrid( + final int territoryNumber, + @Nonnull final Point pointToEncode) { + int codexm = Data.getCodex(territoryNumber); final int orgcodex = codexm; if (codexm == 21) { codexm = 22; @@ -179,18 +217,18 @@ private static String encodeGrid(final int m, final Point pointToEncode) { final int prelen = codexm / 10; final int postlen = codexm % 10; final int divx; - int divy = dataModel.getSmartDiv(m); + int divy = DATA_MODEL.getSmartDiv(territoryNumber); if (divy == 1) { - divx = xSide[prelen]; - divy = ySide[prelen]; + divx = X_SIDE[prelen]; + divy = Y_SIDE[prelen]; } else { - divx = nc[prelen] / divy; + divx = NC[prelen] / divy; } - final int minx = createFromTerritoryRecord(m).getLonMicroDegMin(); - final int miny = createFromTerritoryRecord(m).getLatMicroDegMin(); - final int maxx = createFromTerritoryRecord(m).getLonMicroDegMax(); - final int maxy = createFromTerritoryRecord(m).getLatMicroDegMax(); + final int minx = createBoundaryForTerritoryRecord(territoryNumber).getLonMicroDegMin(); + final int miny = createBoundaryForTerritoryRecord(territoryNumber).getLatMicroDegMin(); + final int maxx = createBoundaryForTerritoryRecord(territoryNumber).getLonMicroDegMax(); + final int maxy = createBoundaryForTerritoryRecord(territoryNumber).getLatMicroDegMax(); final int ygridsize = (((maxy - miny) + divy) - 1) / divy; int rely = pointToEncode.getLatMicroDeg() - miny; @@ -230,8 +268,8 @@ private static String encodeGrid(final int m, final Point pointToEncode) { rely = miny + (rely * ygridsize); relx = minx + (relx * xgridsize); - final int dividery = ((ygridsize + ySide[postlen]) - 1) / ySide[postlen]; - final int dividerx = ((xgridsize + xSide[postlen]) - 1) / xSide[postlen]; + final int dividery = ((ygridsize + Y_SIDE[postlen]) - 1) / Y_SIDE[postlen]; + final int dividerx = ((xgridsize + X_SIDE[postlen]) - 1) / X_SIDE[postlen]; result += '.'; @@ -244,12 +282,12 @@ private static String encodeGrid(final int m, final Point pointToEncode) { difx = difx / dividerx; dify = dify / dividery; - dify = ySide[postlen] - 1 - dify; + dify = Y_SIDE[postlen] - 1 - dify; if (postlen == 3) { result += encodeTriple(difx, dify); } else { - String postfix = encodeBase31(((difx) * ySide[postlen]) + dify, postlen); + String postfix = encodeBase31(((difx) * Y_SIDE[postlen]) + dify, postlen); if (postlen == 4) { postfix = String.valueOf(postfix.charAt(0)) + postfix.charAt(2) + postfix.charAt(1) + postfix.charAt(3); } @@ -262,16 +300,19 @@ private static String encodeGrid(final int m, final Point pointToEncode) { result += encodeExtension(pointToEncode, extrax << 2, extray, dividerx << 2, dividery, 1); // grid - return Data.headerLetter(m) + result; + return Data.headerLetter(territoryNumber) + result; } - private static String encodeAutoHeader(final Point pointToEncode, final int thisindex) { - final StringBuilder autoheader_result = new StringBuilder(); - final int codexm = Data.getCodex(thisindex); + @Nonnull + private static String encodeAutoHeader( + @Nonnull final Point pointToEncode, + final int territoryRecord) { + final StringBuilder stringBuilder = new StringBuilder(); + final int codexm = Data.getCodex(territoryRecord); int storageStart = 0; // search back to first pipe star - int firstindex = thisindex; + int firstindex = territoryRecord; while ((Data.getTerritoryRecordType(firstindex - 1) > Data.TERRITORY_RECORD_TYPE_PIPE) && (Data.getCodex(firstindex - 1) == codexm)) { firstindex--; } @@ -279,10 +320,10 @@ private static String encodeAutoHeader(final Point pointToEncode, final int this int i = firstindex; while (true) { - final int maxx = createFromTerritoryRecord(i).getLonMicroDegMax(); - final int maxy = createFromTerritoryRecord(i).getLatMicroDegMax(); - final int minx = createFromTerritoryRecord(i).getLonMicroDegMin(); - final int miny = createFromTerritoryRecord(i).getLatMicroDegMin(); + final int maxx = createBoundaryForTerritoryRecord(i).getLonMicroDegMax(); + final int maxy = createBoundaryForTerritoryRecord(i).getLatMicroDegMax(); + final int minx = createBoundaryForTerritoryRecord(i).getLonMicroDegMin(); + final int miny = createBoundaryForTerritoryRecord(i).getLatMicroDegMin(); int h = ((maxy - miny) + 89) / 90; final int xdiv = xDivider(miny, maxy); @@ -300,7 +341,7 @@ private static String encodeAutoHeader(final Point pointToEncode, final int this ((((storageStart + product + goodRounder) - 1) / goodRounder) * goodRounder) - storageStart; } - if (i == thisindex) { + if (i == territoryRecord) { final int dividerx = (((maxx - minx) + w) - 1) / w; final int vx = (pointToEncode.getLonMicroDeg() - minx) / dividerx; final int extrax = (pointToEncode.getLonMicroDeg() - minx) % dividerx; @@ -317,13 +358,13 @@ private static String encodeAutoHeader(final Point pointToEncode, final int this value += (vy / 176); final int codexlen = (codexm / 10) + (codexm % 10); - autoheader_result.append(encodeBase31((storageStart / (961 * 31)) + value, codexlen - 2)); - autoheader_result.append('.'); - autoheader_result.append(encodeTriple(vx % 168, vy % 176)); + stringBuilder.append(encodeBase31((storageStart / (961 * 31)) + value, codexlen - 2)); + stringBuilder.append('.'); + stringBuilder.append(encodeTriple(vx % 168, vy % 176)); - autoheader_result.append( + stringBuilder.append( encodeExtension(pointToEncode, extrax << 2, extray, dividerx << 2, dividery, -1)); // AutoHeader - return autoheader_result.toString(); + return stringBuilder.toString(); } storageStart += product; @@ -331,17 +372,20 @@ private static String encodeAutoHeader(final Point pointToEncode, final int this } } - // mid-level encode/decode - private static String encodeNameless(final Point pointToEncode, final int index, final int firstcode) - // returns "" in case of (argument) error - { - final int codexm = Data.getCodex(index); + @Nonnull + private static String encodeNameless( + @Nonnull final Point pointToEncode, + final int territoryRecord, + final int firstTerritoryRecord) { + // mid-level encode/decode + // returns "" in case of (argument) error + final int codexm = Data.getCodex(territoryRecord); final int codexlen = (codexm / 10) + (codexm % 10); - final int first_nameless_record = getFirstNamelessRecord(codexm, index, firstcode); - final int a = countCityCoordinatesForCountry(codexm, index, firstcode); + final int firstNamelessRecord = getFirstNamelessRecord(codexm, territoryRecord, firstTerritoryRecord); + final int a = countCityCoordinatesForCountry(codexm, territoryRecord, firstTerritoryRecord); final int p = 31 / a; final int r = 31 % a; - final int nrX = index - first_nameless_record; + final int nrX = territoryRecord - firstNamelessRecord; int storage_offset; @@ -368,13 +412,13 @@ private static String encodeNameless(final Point pointToEncode, final int index, storage_offset = nrX * basePowerA; } - int side = dataModel.getSmartDiv(index); + int side = DATA_MODEL.getSmartDiv(territoryRecord); final int orgSide = side; int xSide = side; - final int maxy = createFromTerritoryRecord(index).getLatMicroDegMax(); - final int minx = createFromTerritoryRecord(index).getLonMicroDegMin(); - final int miny = createFromTerritoryRecord(index).getLatMicroDegMin(); + final int maxy = createBoundaryForTerritoryRecord(territoryRecord).getLatMicroDegMax(); + final int minx = createBoundaryForTerritoryRecord(territoryRecord).getLonMicroDegMin(); + final int miny = createBoundaryForTerritoryRecord(territoryRecord).getLatMicroDegMin(); final int dividerx4 = xDivider(miny, maxy); final int xFracture = pointToEncode.getLonFraction() / 810000; @@ -393,7 +437,7 @@ private static String encodeNameless(final Point pointToEncode, final int index, } int v = storage_offset; - if (Data.isSpecialShape(index)) { + if (Data.isSpecialShape(territoryRecord)) { xSide *= side; side = 1 + ((maxy - miny) / 90); xSide = xSide / side; @@ -407,7 +451,7 @@ private static String encodeNameless(final Point pointToEncode, final int index, if (codexlen == 3) { result = result.substring(0, 2) + '.' + result.substring(2); } else if (codexlen == 4) { - if ((codexm == 22) && (a < 62) && (orgSide == 961) && !Data.isSpecialShape(index)) { + if ((codexm == 22) && (a < 62) && (orgSide == 961) && !Data.isSpecialShape(territoryRecord)) { result = result.substring(0, 2) + result.charAt(3) + result.charAt(2) + result.charAt(4); } if (codexm == 13) { @@ -421,8 +465,11 @@ private static String encodeNameless(final Point pointToEncode, final int index, return result; } - // add vowels to prevent all-digit mapcodes - static String aeuPack(final String argStr, final boolean argShort) { + @Nonnull + static String aeuPack( + @Nonnull final String argStr, + final boolean argShort) { + // add vowels to prevent all-digit mapcodes String str = argStr; int dotpos = -9; int rlen = str.length(); @@ -457,7 +504,10 @@ static String aeuPack(final String argStr, final boolean argShort) { return str + rest; } - private static String encodeBase31(final int argValue, final int argNrChars) { + @Nonnull + private static String encodeBase31( + final int argValue, + final int argNrChars) { int value = argValue; int nrChars = argNrChars; final StringBuilder result = new StringBuilder(); @@ -469,8 +519,12 @@ private static String encodeBase31(final int argValue, final int argNrChars) { return result.toString(); } - private static int encodeSixWide(final int x, final int y, final int width, final int height) { - int d; + private static int encodeSixWide( + final int x, + final int y, + final int width, + final int height) { + final int d; int col = x / 6; final int maxcol = (width - 4) / 6; if (col >= maxcol) { @@ -482,7 +536,10 @@ private static int encodeSixWide(final int x, final int y, final int width, fina return ((height * 6 * col) + ((height - 1 - y) * d) + x) - (col * 6); } - private static String encodeTriple(final int difx, final int dify) { + @Nonnull + private static String encodeTriple( + final int difx, + final int dify) { if (dify < (4 * 34)) { return ENCODE_CHARS[((difx / 28) + (6 * (dify / 34)))] + encodeBase31(((difx % 28) * 34) + (dify % 34), 2); } else { diff --git a/src/main/java/com/mapcode/Mapcode.java b/src/main/java/com/mapcode/Mapcode.java index c7adfcb..f7ef818 100644 --- a/src/main/java/com/mapcode/Mapcode.java +++ b/src/main/java/com/mapcode/Mapcode.java @@ -75,9 +75,8 @@ public final class Mapcode { * @throws IllegalArgumentException Thrown if syntax not valid or if the mapcode string contains * territory information. */ - public Mapcode( - @Nonnull final String code, - @Nonnull final Territory territory) throws IllegalArgumentException { + public Mapcode(@Nonnull final String code, + @Nonnull final Territory territory) throws IllegalArgumentException { checkMapcodeCode("code", code); final String ascii = convertStringToPlainAscii(code); @@ -90,20 +89,17 @@ public Mapcode( final int hyphenPos = codeUppercase.indexOf('-'); if (hyphenPos < 0) { codeUppercase = codeUppercase + "-K3000000"; - } - else { + } else { final int extensionLength = codeUppercase.length() - 1 - hyphenPos; if (extensionLength < 8) { if ((extensionLength % 2) == 1) { // Odd extension. codeUppercase = codeUppercase + ("HH000000".substring(0, 8 - extensionLength)); - } - else { + } else { // Even extension. codeUppercase = codeUppercase + ("K3000000".substring(0, 8 - extensionLength)); } - } - else if (extensionLength > 8) { + } else if (extensionLength > 8) { // Cut to 8 characters. codeUppercase = codeUppercase.substring(0, hyphenPos + 9); } @@ -157,12 +153,10 @@ public String getCode() { public String getCode(final int precision, @Nullable final Alphabet alphabet) { if (precision == 0) { return convertStringToAlphabet(codePrecision8.substring(0, codePrecision8.length() - 9), alphabet); - } - else if (precision <= 8) { + } else if (precision <= 8) { return convertStringToAlphabet(codePrecision8.substring(0, (codePrecision8.length() - 8) + precision), - alphabet); - } - else { + alphabet); + } else { throw new IllegalArgumentException("getCodePrecision: precision must be in [0, 8]"); } } @@ -256,11 +250,11 @@ public Territory getTerritory() { * provided as statics to only compile these patterns once. */ @Nonnull - static final String REGEX_TERRITORY = "[\\p{L}\\p{N}]{2,3}+([-_][\\p{L}\\p{N}]{2,3}+)?"; + static final String REGEX_TERRITORY = "[\\p{L}\\p{N}]{2,3}+([-_][\\p{L}\\p{N}]{2,3}+)?"; @Nonnull - static final String REGEX_CODE_PREFIX = "[\\p{L}\\p{N}]{2,5}+"; + static final String REGEX_CODE_PREFIX = "[\\p{L}\\p{N}]{2,5}+"; @Nonnull - static final String REGEX_CODE_POSTFIX = "[\\p{L}\\p{N}]{2,4}+"; + static final String REGEX_CODE_POSTFIX = "[\\p{L}\\p{N}]{2,4}+"; @Nonnull static final String REGEX_CODE_PRECISION = "[-][\\p{L}\\p{N}&&[^zZ]]{1,8}+"; @@ -270,10 +264,10 @@ public Territory getTerritory() { */ @Nonnull public static final String REGEX_MAPCODE = '(' + REGEX_TERRITORY + "[ ]+)?" + - REGEX_CODE_PREFIX + "[.]" + REGEX_CODE_POSTFIX + '(' + REGEX_CODE_PRECISION + ")?"; + REGEX_CODE_PREFIX + "[.]" + REGEX_CODE_POSTFIX + '(' + REGEX_CODE_PRECISION + ")?"; @Nonnull - static final Pattern PATTERN_MAPCODE = Pattern.compile('^' + REGEX_MAPCODE + '$'); + static final Pattern PATTERN_MAPCODE = Pattern.compile('^' + REGEX_MAPCODE + '$'); @Nonnull static final Pattern PATTERN_TERRITORY = Pattern.compile('^' + REGEX_TERRITORY + ' '); @Nonnull @@ -298,7 +292,7 @@ public static int getPrecisionFormat(@Nonnull final String mapcode) throws Unkno // Syntax needs to be OK. if (!PATTERN_MAPCODE.matcher(decodedMapcode).matches()) { throw new UnknownPrecisionFormatException(decodedMapcode + " is not a correctly formatted mapcode code; " + - "the regular expression for the mapcode code syntax is: " + REGEX_MAPCODE); + "the regular expression for the mapcode code syntax is: " + REGEX_MAPCODE); } // Precision part should be OK. @@ -325,8 +319,7 @@ public static boolean isValidMapcodeFormat(@Nonnull final String mapcode) throws // Throws an exception if the format is incorrect. getPrecisionFormat(mapcode.toUpperCase()); return true; - } - catch (final UnknownPrecisionFormatException ignored) { + } catch (final UnknownPrecisionFormatException ignored) { return false; } } @@ -348,15 +341,15 @@ public static boolean containsTerritory(@Nonnull final String mapcode) throws Il * location used for encoding the mapcode. */ private static final double[] PRECISION_0_MAX_OFFSET_METERS = { - 7.49, // PRECISION_0: 7.49 meters or less +/- 7.5 m - 1.39, // PRECISION_1: 1.39 meters or less +/- 1.4 m - 0.251, // PRECISION_2: 25.1 cm or less +/- 25 cm - 0.0462, // PRECISION_3: 4.62 cm or less +/- 5 cm - 0.00837, // PRECISION_4: 8.37 mm or less +/- 1 cm - 0.00154, // PRECISION_5: 1.54 mm or less +/- 2 mm - 0.000279, // PRECISION_6: 279 micrometer or less +/- 1/3 mm - 0.0000514, // PRECISION_7: 51.4 micrometer or less +/- 1/20 mm - 0.0000093 // PRECISION_8: 9.3 micrometer or less +/- 1/100 mm + 7.49, // PRECISION_0: 7.49 meters or less +/- 7.5 m + 1.39, // PRECISION_1: 1.39 meters or less +/- 1.4 m + 0.251, // PRECISION_2: 25.1 cm or less +/- 25 cm + 0.0462, // PRECISION_3: 4.62 cm or less +/- 5 cm + 0.00837, // PRECISION_4: 8.37 mm or less +/- 1 cm + 0.00154, // PRECISION_5: 1.54 mm or less +/- 2 mm + 0.000279, // PRECISION_6: 279 micrometer or less +/- 1/3 mm + 0.0000514, // PRECISION_7: 51.4 micrometer or less +/- 1/20 mm + 0.0000093 // PRECISION_8: 9.3 micrometer or less +/- 1/100 mm }; /** @@ -400,7 +393,7 @@ static String convertStringToPlainAscii(@Nonnull final String string) { @Nonnull static String convertStringToAlphabet(@Nonnull final String string, @Nullable final Alphabet alphabet) throws IllegalArgumentException { return (alphabet != null) ? Decoder.encodeUTF16(string.toUpperCase(), alphabet.getNumber()) : - string.toUpperCase(); + string.toUpperCase(); } /** @@ -430,6 +423,6 @@ public boolean equals(@Nullable final Object obj) { } final Mapcode that = (Mapcode) obj; return this.territory.equals(that.territory) && - this.codePrecision8.equals(that.codePrecision8); + this.codePrecision8.equals(that.codePrecision8); } } diff --git a/src/main/java/com/mapcode/MapcodeCodec.java b/src/main/java/com/mapcode/MapcodeCodec.java index b3a57d7..385d8ed 100644 --- a/src/main/java/com/mapcode/MapcodeCodec.java +++ b/src/main/java/com/mapcode/MapcodeCodec.java @@ -34,7 +34,8 @@ */ public final class MapcodeCodec { - private static final DataModel dataModel = DataModel.getInstance(); + // Get direct access to the data model. + private static final DataModel DATA_MODEL = DataModel.getInstance(); private MapcodeCodec() { // Prevent instantiation. @@ -98,15 +99,13 @@ public static List encode(@Nonnull final Point point) public static List encode(final double latDeg, final double lonDeg, @Nullable final Territory restrictToTerritory) throws IllegalArgumentException { - // Call Mapcode encoder. - final List results = Encoder.encode(latDeg, lonDeg, restrictToTerritory, /* Stop with one result: */ false); + final List results = Encoder.encode(latDeg, lonDeg, restrictToTerritory, false); assert results != null; return results; } @Nonnull - public static List encode(@Nonnull final Point point, - @Nullable final Territory restrictToTerritory) + public static List encode(@Nonnull final Point point, @Nullable final Territory restrictToTerritory) throws IllegalArgumentException { checkNonnull("point", point); return encode(point.getLatDeg(), point.getLonDeg(), restrictToTerritory); @@ -220,50 +219,62 @@ public static Point decode(@Nonnull final String mapcode) */ @SuppressWarnings("DuplicateThrows") @Nonnull - public static Point decode( - @Nonnull final String mapcode, - @Nullable final Territory defaultTerritoryContext) + public static Point decode(@Nonnull final String mapcode, @Nullable final Territory defaultTerritoryContext) throws UnknownMapcodeException, IllegalArgumentException, UnknownPrecisionFormatException { checkNonnull("mapcode", mapcode); - // Clean up mapcode. - String mapcodeClean = Mapcode.convertStringToPlainAscii(mapcode.trim()).toUpperCase(); - - // Determine territory from mapcode. - final Territory territory; - final Matcher matcherTerritory = Mapcode.PATTERN_TERRITORY.matcher(mapcodeClean); - if (!matcherTerritory.find()) { - - // No territory code was supplied in the string, use specified territory context parameter. - territory = (defaultTerritoryContext != null) ? defaultTerritoryContext : Territory.AAA; - } else { - - // Use the territory code from the string. - final String territoryName = mapcodeClean.substring(matcherTerritory.start(), matcherTerritory.end()).trim(); - try { - territory = Territory.fromString(territoryName); - } catch (final UnknownTerritoryException ignored) { - throw new UnknownMapcodeException("Wrong territory code: " + territoryName); - } - - // Cut off the territory part. - mapcodeClean = mapcodeClean.substring(matcherTerritory.end()).trim(); + final MapcodeZone mapcodeZone = decodeToMapcodeZone(mapcode, defaultTerritoryContext); + if (mapcodeZone.isEmpty()) { + throw new UnknownMapcodeException("Unknown mapcode, mapcode=" + mapcode + ", territoryContext=" + defaultTerritoryContext); } + return mapcodeZone.getCenter(); + } - // Throws an exception if the format is incorrect. - getPrecisionFormat(mapcodeClean); - - @Nonnull final Point point = Decoder.decode(mapcodeClean, territory); - assert point != null; + /** + * Decode a mapcode to a Rectangle, which defines the valid zone for a mapcode. The boundaries of the + * mapcode zone are inclusive for the South and West borders and exclusive for the North and East borders. + * This is essentially the same call as a 'decode', except it returns a rectangle, rather than its center point. + * + * @param mapcode Mapcode. + * @return Rectangle Mapcode zone. South/West borders are inclusive, North/East borders exclusive. + * @throws UnknownMapcodeException Thrown if the mapcode has the correct syntax, + * but cannot be decoded into a point. + * @throws UnknownPrecisionFormatException Thrown if the precision format is incorrect. + * @throws IllegalArgumentException Thrown if arguments are null, or if the syntax of the mapcode is incorrect. + */ + @Nonnull + public static Rectangle decodeToRectangle(@Nonnull final String mapcode) + throws UnknownMapcodeException, IllegalArgumentException, UnknownPrecisionFormatException { + return decodeToRectangle(mapcode, Territory.AAA); + } - // Points can only be undefined within the mapcode implementation. Throw an exception here if undefined. - if (!point.isDefined()) { - throw new UnknownMapcodeException("Unknown Mapcode: " + mapcodeClean + - ", territoryContext=" + defaultTerritoryContext); - } - return point; + /** + * Decode a mapcode to a Rectangle, which defines the valid zone for a mapcode. The boundaries of the + * mapcode zone are inclusive for the South and West borders and exclusive for the North and East borders. + * This is essentially the same call as a 'decode', except it returns a rectangle, rather than its center point. + * + * @param mapcode Mapcode. + * @param defaultTerritoryContext Default territory context for disambiguation purposes. May be null. + * @return Rectangle Mapcode zone. South/West borders are inclusive, North/East borders exclusive. + * @throws UnknownMapcodeException Thrown if the mapcode has the correct syntax, + * but cannot be decoded into a point. + * @throws UnknownPrecisionFormatException Thrown if the precision format is incorrect. + * @throws IllegalArgumentException Thrown if arguments are null, or if the syntax of the mapcode is incorrect. + */ + @Nonnull + public static Rectangle decodeToRectangle(@Nonnull final String mapcode, @Nullable final Territory defaultTerritoryContext) + throws UnknownMapcodeException, IllegalArgumentException, UnknownPrecisionFormatException { + checkNonnull("mapcode", mapcode); + final MapcodeZone mapcodeZone = decodeToMapcodeZone(mapcode, defaultTerritoryContext); + final Point southWest = Point.fromLatLonFractions(mapcodeZone.getLatFractionMin(), mapcodeZone.getLonFractionMin()); + final Point northEast = Point.fromLatLonFractions(mapcodeZone.getLatFractionMax(), mapcodeZone.getLonFractionMax()); + final Rectangle rectangle = new Rectangle(southWest, northEast); + assert rectangle.isDefined(); + return rectangle; } + + // TODO: Explain when this method is needed/used. /** * Is coordinate near multiple territory borders? * @@ -274,7 +285,7 @@ public static Point decode( public static boolean isNearMultipleBorders(@Nonnull final Point point, @Nonnull final Territory territory) { checkDefined("point", point); if (territory != Territory.AAA) { - final int ccode = territory.getNumber(); + final int territoryNumber = territory.getNumber(); if (territory.getParentTerritory() != null) { // There is a parent! check its borders as well... if (isNearMultipleBorders(point, territory.getParentTerritory())) { @@ -282,11 +293,11 @@ public static boolean isNearMultipleBorders(@Nonnull final Point point, @Nonnull } } int nrFound = 0; - final int fromTerritoryRecord = dataModel.getDataFirstRecord(ccode); - final int uptoTerritoryRecord = dataModel.getDataLastRecord(ccode); + final int fromTerritoryRecord = DATA_MODEL.getDataFirstRecord(territoryNumber); + final int uptoTerritoryRecord = DATA_MODEL.getDataLastRecord(territoryNumber); for (int territoryRecord = uptoTerritoryRecord; territoryRecord >= fromTerritoryRecord; territoryRecord--) { if (!Data.isRestricted(territoryRecord)) { - final Boundary boundary = Boundary.createFromTerritoryRecord(territoryRecord); + final Boundary boundary = Boundary.createBoundaryForTerritoryRecord(territoryRecord); final int xdiv8 = Common.xDivider(boundary.getLatMicroDegMin(), boundary.getLatMicroDegMax()) / 4; if (boundary.extendBoundary(60, xdiv8).containsPoint(point)) { if (!boundary.extendBoundary(-60, -xdiv8).containsPoint(point)) { @@ -301,4 +312,36 @@ public static boolean isNearMultipleBorders(@Nonnull final Point point, @Nonnull } return false; } + + @Nonnull + private static MapcodeZone decodeToMapcodeZone(@Nonnull final String mapcode, @Nullable final Territory defaultTerritoryContext) + throws UnknownMapcodeException, IllegalArgumentException, UnknownPrecisionFormatException { + checkNonnull("mapcode", mapcode); + String mapcodeClean = Mapcode.convertStringToPlainAscii(mapcode.trim()).toUpperCase(); + + // Determine territory from mapcode. + final Territory territory; + final Matcher matcherTerritory = Mapcode.PATTERN_TERRITORY.matcher(mapcodeClean); + if (!matcherTerritory.find()) { + + // No territory code was supplied in the string, use specified territory context parameter. + territory = (defaultTerritoryContext != null) ? defaultTerritoryContext : Territory.AAA; + } else { + + // Use the territory code from the string. + final String territoryName = mapcodeClean.substring(matcherTerritory.start(), matcherTerritory.end()).trim(); + try { + territory = Territory.fromString(territoryName); + } catch (final UnknownTerritoryException ignored) { + throw new UnknownMapcodeException("Wrong territory code: " + territoryName); + } + + // Cut off the territory part. + mapcodeClean = mapcodeClean.substring(matcherTerritory.end()).trim(); + } + + // Throws an exception if the format is incorrect. + getPrecisionFormat(mapcodeClean); + return Decoder.decodeToMapcodeZone(mapcodeClean, territory); + } } diff --git a/src/main/java/com/mapcode/MapcodeZone.java b/src/main/java/com/mapcode/MapcodeZone.java index cf13ed1..208da94 100644 --- a/src/main/java/com/mapcode/MapcodeZone.java +++ b/src/main/java/com/mapcode/MapcodeZone.java @@ -22,10 +22,12 @@ * ---------------------------------------------------------------------------------------------- * Package private implementation class. For internal use within the Mapcode implementation only. * ---------------------------------------------------------------------------------------------- + * * Simple class to represent all the coordinates that would deliver a particular mapcode. */ class MapcodeZone { + // TODO: Explain why you need these fractions and how they work exactly. // Longitudes in LonFractions ("1/3240 billionths"). private double lonFractionMin; private double lonFractionMax; @@ -34,35 +36,20 @@ class MapcodeZone { private double latFractionMin; private double latFractionMax; - // Construct an (empty) zone. - MapcodeZone() { - setEmpty(); - } - - // Construct an (empty) zone. - void setEmpty() { - latFractionMin = 0; - latFractionMax = 0; - lonFractionMin = 0; - lonFractionMax = 0; - } - - // Construct a copy of an existing zone. - MapcodeZone(@Nonnull final MapcodeZone zone) { - copyFrom(zone); + MapcodeZone(final double latFractionMin, final double latFractionMax, + final double lonFractionMin, final double lonFractionMax) { + this.latFractionMin = latFractionMin; + this.latFractionMax = latFractionMax; + this.lonFractionMin = lonFractionMin; + this.lonFractionMax = lonFractionMax; } - // construct a copy of an existing zone - void copyFrom(@Nonnull final MapcodeZone other) { - latFractionMin = other.latFractionMin; - latFractionMax = other.latFractionMax; - lonFractionMin = other.lonFractionMin; - lonFractionMax = other.lonFractionMax; + MapcodeZone(@Nonnull final MapcodeZone mapcodeZone) { + this(mapcodeZone.latFractionMin, mapcodeZone.latFractionMax, mapcodeZone.lonFractionMin, mapcodeZone.lonFractionMax); } - @Nonnull - static MapcodeZone empty() { - return new MapcodeZone(); + MapcodeZone() { + this(0.0, 0.0, 0.0, 0.0); } double getLonFractionMin() { @@ -97,10 +84,10 @@ void setLatFractionMax(final double latFractionMax) { this.latFractionMax = latFractionMax; } + // TODO: Use of this method is unclear. // Generate upper and lower limits based on x and y, and delta's. - void setFromFractions( - final double latFraction, final double lonFraction, - final double latFractionDelta, final double lonFractionDelta) { + void setFromFractions(final double latFraction, final double lonFraction, + final double latFractionDelta, final double lonFractionDelta) { assert (lonFractionDelta >= 0.0); assert (latFractionDelta != 0.0); lonFractionMin = lonFraction; @@ -118,55 +105,58 @@ boolean isEmpty() { return ((lonFractionMax <= lonFractionMin) || (latFractionMax <= latFractionMin)); } + // TODO: Explain what this is (geo point of mapcode). @Nonnull - Point getMidPoint() { + Point getCenter() { if (isEmpty()) { return Point.undefined(); } else { - final double latFrac = Math.floor((latFractionMin + latFractionMax) / 2); - final double lonFrac = Math.floor((lonFractionMin + lonFractionMax) / 2); + final double latFrac = Math.floor((latFractionMin + latFractionMax) / 2.0); + final double lonFrac = Math.floor((lonFractionMin + lonFractionMax) / 2.0); return Point.fromLatLonFractions(latFrac, lonFrac); } } + // TODO: Explain when this is used. It clips a zone to an encompassing boundary. // Returns a non-empty intersection of a mapcode zone and a territory area. // Returns null if no such intersection exists. @Nonnull MapcodeZone restrictZoneTo(@Nonnull final Boundary area) { - MapcodeZone z = new MapcodeZone(this); + final MapcodeZone mapcodeZone = new MapcodeZone(latFractionMin, latFractionMax, lonFractionMin, lonFractionMax); final double latMin = area.getLatMicroDegMin() * Point.LAT_MICRODEG_TO_FRACTIONS_FACTOR; - if (z.latFractionMin < latMin) { - z.latFractionMin = latMin; + if (mapcodeZone.latFractionMin < latMin) { + mapcodeZone.latFractionMin = latMin; } final double latMax = area.getLatMicroDegMax() * Point.LAT_MICRODEG_TO_FRACTIONS_FACTOR; - if (z.latFractionMax > latMax) { - z.latFractionMax = latMax; + if (mapcodeZone.latFractionMax > latMax) { + mapcodeZone.latFractionMax = latMax; } - if (z.latFractionMin < z.latFractionMax) { + if (mapcodeZone.latFractionMin < mapcodeZone.latFractionMax) { double lonMin = area.getLonMicroDegMin() * Point.LON_MICRODEG_TO_FRACTIONS_FACTOR; double lonMax = area.getLonMicroDegMax() * Point.LON_MICRODEG_TO_FRACTIONS_FACTOR; - if ((lonMax < 0) && (z.lonFractionMin > 0)) { + if ((lonMax < 0) && (mapcodeZone.lonFractionMin > 0)) { lonMin += (Point.MICRO_DEG_360 * Point.LON_MICRODEG_TO_FRACTIONS_FACTOR); lonMax += (Point.MICRO_DEG_360 * Point.LON_MICRODEG_TO_FRACTIONS_FACTOR); - } else if ((lonMin > 1) && (z.lonFractionMax < 0)) { + } else if ((lonMin > 1) && (mapcodeZone.lonFractionMax < 0)) { lonMin -= (Point.MICRO_DEG_360 * Point.LON_MICRODEG_TO_FRACTIONS_FACTOR); lonMax -= (Point.MICRO_DEG_360 * Point.LON_MICRODEG_TO_FRACTIONS_FACTOR); } - if (z.lonFractionMin < lonMin) { - z.lonFractionMin = lonMin; + if (mapcodeZone.lonFractionMin < lonMin) { + mapcodeZone.lonFractionMin = lonMin; } - if (z.lonFractionMax > lonMax) { - z.lonFractionMax = lonMax; + if (mapcodeZone.lonFractionMax > lonMax) { + mapcodeZone.lonFractionMax = lonMax; } } - return z; + return mapcodeZone; } @Nonnull @Override public String toString() { - return isEmpty() ? "empty" : - ("[" + (latFractionMin / Point.LAT_TO_FRACTIONS_FACTOR) + ", " + (latFractionMax / Point.LAT_TO_FRACTIONS_FACTOR) + - "), [" + (lonFractionMin / Point.LON_TO_FRACTIONS_FACTOR) + ", " + (lonFractionMax / Point.LON_TO_FRACTIONS_FACTOR) + ')'); + return isEmpty() ? "empty" : ("[" + (latFractionMin / Point.LAT_TO_FRACTIONS_FACTOR) + ", " + + (latFractionMax / Point.LAT_TO_FRACTIONS_FACTOR) + + "), [" + (lonFractionMin / Point.LON_TO_FRACTIONS_FACTOR) + ", " + + (lonFractionMax / Point.LON_TO_FRACTIONS_FACTOR) + ')'); } } diff --git a/src/main/java/com/mapcode/Point.java b/src/main/java/com/mapcode/Point.java index 7de9ccb..2d6e408 100644 --- a/src/main/java/com/mapcode/Point.java +++ b/src/main/java/com/mapcode/Point.java @@ -220,7 +220,7 @@ public String toString() { @SuppressWarnings("NonFinalFieldReferencedInHashCode") @Override public int hashCode() { - return Arrays.hashCode(new Object[]{getLatDeg(), getLonDeg(), defined}); + return Arrays.hashCode(new Object[]{latMicroDeg, lonMicroDeg, latFractionOnlyDeg, lonFractionOnlyDeg, defined}); } @SuppressWarnings("NonFinalFieldReferenceInEquals") diff --git a/src/main/java/com/mapcode/Rectangle.java b/src/main/java/com/mapcode/Rectangle.java new file mode 100644 index 0000000..426a89a --- /dev/null +++ b/src/main/java/com/mapcode/Rectangle.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016-2017, Stichting Mapcode Foundation (http://www.mapcode.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mapcode; + +import javax.annotation.Nonnull; +import java.util.Arrays; + +/** + * This class defines a geospatial rectangle, defined by a South/West and a North/East + * point. The rectangle is undefined if either of the corner points are undefined. + */ +public class Rectangle { + + private final Point southWest; + private final Point northEast; + + Rectangle(@Nonnull final Point southWest, @Nonnull final Point northEast) { + this.southWest = southWest; + this.northEast = northEast; + } + + @Nonnull + public Point getSouthWest() { + return southWest; + } + + @Nonnull + public Point getNorthEast() { + return northEast; + } + + @Nonnull + public Point getCenter() { + if (!isDefined()) { + return Point.undefined(); + } + final double centerLat = ((southWest.getLatDeg() + northEast.getLatDeg()) / 2.0); + final double centerLon = ((southWest.getLonDeg() + northEast.getLonDeg()) / 2.0); + return Point.fromDeg(centerLat, centerLon); + } + + boolean isDefined() { + return southWest.isDefined() && northEast.isDefined(); + } + + @Nonnull + @Override + public String toString() { + return "[" + southWest + ", " + northEast + ']'; + } + + @SuppressWarnings("NonFinalFieldReferencedInHashCode") + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{southWest, northEast}); + } + + @SuppressWarnings("NonFinalFieldReferenceInEquals") + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Rectangle)) { + return false; + } + final Rectangle that = (Rectangle) obj; + return (this.southWest.equals(that.southWest) && + this.northEast.equals(that.northEast)); + } +} diff --git a/src/main/java/com/mapcode/Territory.java b/src/main/java/com/mapcode/Territory.java index 043a06e..d7c23f1 100644 --- a/src/main/java/com/mapcode/Territory.java +++ b/src/main/java/com/mapcode/Territory.java @@ -18,14 +18,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import static com.mapcode.CheckArgs.checkNonnull; @@ -702,9 +695,8 @@ public static Territory fromString(@Nonnull final String alphaCode) throws Unkno * @throws UnknownTerritoryException Thrown if the territory is not found given the parentTerritory. */ @Nonnull - public static Territory fromString( - @Nonnull final String alphaCode, - @Nonnull final Territory parentTerritory) throws UnknownTerritoryException { + public static Territory fromString(@Nonnull final String alphaCode, + @Nonnull final Territory parentTerritory) throws UnknownTerritoryException { checkNonnull("alphaCode", alphaCode); checkNonnull("parentTerritory", parentTerritory); if (!PARENT_TERRITORIES.contains(parentTerritory)) { @@ -803,43 +795,38 @@ public boolean hasSubdivisions() { /** * Private constructors to create a territory code. */ - private Territory( - final int number, - @Nonnull final String fullName) { + private Territory(final int number, + @Nonnull final String fullName) { this(number, fullName, null, null, null, null); } - private Territory( - final int number, - @Nonnull final String fullName, - @Nullable final Alphabet[] alphabets) { + private Territory(final int number, + @Nonnull final String fullName, + @Nullable final Alphabet[] alphabets) { this(number, fullName, alphabets, null, null, null); } - private Territory( - final int number, - @Nonnull final String fullName, - @Nullable final Alphabet[] alphabets, - @Nullable final Territory parentTerritory) { + private Territory(final int number, + @Nonnull final String fullName, + @Nullable final Alphabet[] alphabets, + @Nullable final Territory parentTerritory) { this(number, fullName, alphabets, parentTerritory, null, null); } - private Territory( - final int number, - @Nonnull final String fullName, - @Nullable final Alphabet[] alphabets, - @Nullable final Territory parentTerritory, - @Nullable final String[] aliases) { + private Territory(final int number, + @Nonnull final String fullName, + @Nullable final Alphabet[] alphabets, + @Nullable final Territory parentTerritory, + @Nullable final String[] aliases) { this(number, fullName, alphabets, parentTerritory, aliases, null); } - private Territory( - final int number, - @Nonnull final String fullName, - @Nullable final Alphabet[] alphabets, - @Nullable final Territory parentTerritory, - @Nullable final String[] aliases, - @Nullable final String[] fullNameAliases) { + private Territory(final int number, + @Nonnull final String fullName, + @Nullable final Alphabet[] alphabets, + @Nullable final Territory parentTerritory, + @Nullable final String[] aliases, + @Nullable final String[] fullNameAliases) { assert number >= 0; this.number = number; this.fullName = fullName; @@ -866,7 +853,7 @@ private Territory( parentList = new ArrayList(); int min = Integer.MAX_VALUE; int max = Integer.MIN_VALUE; - final Set territoryCodes = new HashSet(); + final Set territoryNumbers = new HashSet(); final Set namesSet = new HashSet(); for (final Territory territory : Territory.values()) { @@ -879,10 +866,10 @@ private Territory( } // Check if territory code was already used. - if (territoryCodes.contains(territoryNumber)) { + if (territoryNumbers.contains(territoryNumber)) { throw new ExceptionInInitializerError(errorPrefix + "non-unique territory number: " + territoryNumber); } - territoryCodes.add(territory.getNumber()); + territoryNumbers.add(territory.getNumber()); final int initialCodeListSize = codeList.size(); for (int i = initialCodeListSize; i <= territory.number; i++) { @@ -895,7 +882,7 @@ private Territory( // Check if territory name is unique. if (namesSet.contains(territory.toString())) { - throw new ExceptionInInitializerError(errorPrefix + "non-unique territory name: " + territory.toString()); + throw new ExceptionInInitializerError(errorPrefix + "non-unique territory name: " + territory); } namesSet.add(territory.toString()); addNameWithParentVariants(territory.toString(), territory); @@ -927,7 +914,7 @@ private Territory( max = Math.max(max, territory.number); assert territory.alphabets.length > 0; } - assert territoryCodes.size() == Territory.values().length; + assert territoryNumbers.size() == Territory.values().length; // Check that territory has at least one alphabet. @@ -946,9 +933,8 @@ private Territory( * @throws UnknownTerritoryException Thrown if the territory is not found. */ @Nonnull - private static Territory createFromString( - @Nonnull final String alphaCode, - @Nullable final Territory parentTerritory) throws UnknownTerritoryException { + private static Territory createFromString(@Nonnull final String alphaCode, + @Nullable final Territory parentTerritory) throws UnknownTerritoryException { // Replace '_' with '-', but leave spaces alone (may be part of the name). final String trimmed = Mapcode.convertStringToPlainAscii( @@ -992,9 +978,8 @@ private static Territory createFromString( * @param name Name to add. * @param territory Territory. */ - private static void addNameWithParentVariants( - @Nonnull final String name, - @Nonnull final Territory territory) { + private static void addNameWithParentVariants(@Nonnull final String name, + @Nonnull final Territory territory) { // Add the name as provided addNameWithSeperatorVariants(name, territory); diff --git a/src/site/markdown/ReleaseNotes.md b/src/site/markdown/ReleaseNotes.md deleted file mode 100755 index 7b79c57..0000000 --- a/src/site/markdown/ReleaseNotes.md +++ /dev/null @@ -1,280 +0,0 @@ -# Release Notes - -These are the release notes for the Java library for mapcodes. - -### 2.4.3 - -* Updated Maven dependencies for latest patches. - -### 2.4.2 - -* Removed secret Coveralls key from POM file. - -### 2.4.1 - -* Added scripts for Tifinagh (Berber), Tamil, Amharic, Telugu, Odia, Kannada, Gujarati. - -* Added `getAlphabets()` to `Territory` class, returning the most commonly used languages for the territory. - -* Renamed constant `HINDI` to `DEVANAGIRI`. - -* Improved some characters for Arabic and Devanagari. - -* Fixed Bengali to also support Assamese. - -### 2.4.0 - -* Added scripts for Korean (Choson'gul/Hangul), Burmese, Khmer, Sinhalese, Thaana (Maldivan), Chinese (Zhuyin, Bopomofo). - -* Renamed constant `MALAY` to `MALAYALAM`. - -### 2.3.1 - -* Fixed data for some parts of China. - -### 2.3.0 - -* Added Arabic support. - -* Fixed Greek, Hebrew and Hindi support. - -### 2.2.5 - -* Updated documentation. - -* Cleaned up POM, sorted dependencies. - -### 2.2.4 - -* Added Travis CI and Coveralls badges to `README.md`. - -* Replaces static `DataAccess` class with singleton `DataModel` to allow testing -of incorrect data model files. - -* Fixed error handling for incorrect data model files. - -* Fix error to info logging in `aeuUnpack`. - -* Updated all POM dependencies. - -* Updated copyright messages. - -* Improved test coverage of unit tests. - -### 2.2.3 - -* Issue #23: Fixed `Territory.fromString` to make sure the parent territory is valid for - input like "CHE-GR". This returned "MX-GRO" instead of throwing `UnknownTerritoryException`. - Added unit test for this type of case. - -* Fixed minor JavaDoc issues. - -### 2.2.2 - -* Fixed error in `Point` which in rare cases would allow longitudes outside proper range. - -### 2.2.1 - -* Fixed unit test. Reduced size of files for unit tests considerably. Improved unit test speed. - -* Fixed `Point` interface. - -* Cleaned up `Boundary` and `DataAccess`. - -### 2.2.0 - -* Solved 1-microdegree gap in a few spots on Earth, noticable now extreme precision is possible. - -* Replaced floating point by fixed point math. - -* Improved speed. - -* Enforce `Mencode(decode(M)) == M`, except at territory border corners. - -* Cleaned up source; moved hard-coded data into `mminfo.dat`. - -### 2.1.0 - -* Added micro-meter precision (mapcodes can now have eight precision digits). - -* Assure that encode(decode(m)) delivers m. - -* Renames to bring source more in line with other implementations. - -### 2.0.2 - -* Renamed `isValidPrecisionFormat` to `isValidMapcodeFormat`. - -* Removed public microdegree references from `Point` class. Everything is degrees now. - -* Removed `ParentTerritory` class. - -### 2.0.1 - -* Reverted Java JDK level to 1.6 (Java 6) from 1.8 (Java 8), so the library can be used on - Android platforms operating at Java 6 as well. - -* Use multi-threading for long running test to speed them up (uses all CPU cores now). - -* Added the ability to use a country name for `Territory.fromString()`. - -### 2.0.0 - -* Fixes to the data rectangles (primarily intended for ISO proposal). - -* Removed functionality to use numeric territory codes; only alpha codes are accepted. - -* Note that this release only allows high-precision mapcodes up to 2 additional suffix characters. -A future release will be scheduled to allow up to 8 suffix characters (nanometer accuracy). - -### 1.50.3 - -* This release breaks compatiblity with earlier releases, to clean up the interface significantly. - -* Removed `Mapcode.encodeToShortest(lat, lon))` as this will produce a randomly chosen territory. - You must specify a `restrictToTerritory` now. - -* Renamed `Territory.code` to `Territory.number`. - -* Renamed `fromTerritoryCode())` to `fromNumber())`. - -* Renamed `Territory.isState())` to `Territory.isSubdivision())` and - -* Renamed `Territory.hasStates())` to `Territory.hasSubdivision())`. - -* Renamed `Alphabet.code` to `Alphabet.number`. - -* Renamed `fromCode())` to `fromNumber())`. - -* Renamed `MapcodeFormat` to `PrecisionFormat`. - -* Deprecated methods have been removed. - -### 1.50.2 - -* Cleaned up Unicode handling a bit. - -* Speed up of reading initialization data. - -* Rename `toNameFormat` into `toAlphaFormat` and `NAME_FORMAT` to `ALPHA_FORMAT`. - -### 1.50.1 - -* Bugfix for mapcodes in IN-DD (in India). - -### 1.50 - -* Major release. This version is not backwards compatible with mapcode 1.4x: is has dropped support for - Antartica AT0-8 codes and has a changed (improved) way of dealing with the Greek alphabet. - -* Added 22-chararcter post-processing of all-digit mapcodes for the Greek alphabet. - -* Retired legacy aliases EAZ and SKM, AU-QL, AU-TS, AU-NI and AU-JB. - -* Retired legacy Antarctica claims AT0 through AT8. - -* Added convencience methods for `MapcodeCodec` to accept `Point` for all encode functions - as well (not just `latDeg`, `lonDeg`). - -* Added alphabet support to convert mapcodes (both codes and territories) between `Alphabet`s. - -* Exceptions have been corrected and documented in code. - -* Allowed nullable values in `MapcodeCodec` encode and decode methods to assume reasonable defaults. - -* Microdegrees are no longer support publicly in `Point`. Only degrees. - -* Latitudes are limited to -90..90 and longitudes are wrapped to -180..180 (non inclusive). - -### 1.42.3 - -* To be done. - -### 1.42.2 - -* Upper- and lowercase mapcodes always allowed. - -### 1.42.1 - -* Cleaned up source. Removed all pending IntelliJ IDEA inspection warnings and reformatted code - using default IDEA code style to maintain consistent layout. - -* Add additional unit tests to check for correct handling of international mapcode handling. - -* Added safe constants for the maximum delta distance in meters for mapcode accuracy. - -### 1.42 - -* Fixed a bug in `MapcodeCodec.encodeToShortest` which would not always return the shortest code (see - next bullet). Reproducible with `curl -X GET http://localhost:8080/mapcode/to/47.1243/-111.28564/local`. - -* Fixed a bug where `Encoder.encode` would sometime retrieve more than one result even if result set - was limited to 1 result. - -### 1.41.1 - -* Added convenience method to Mapcode. - -### 1.41 - -* Added the India state Telangana (IN-TG), until 2014 a region in Adhra Pradesh. - -* Updated POM dependencies to latest library versions of standard components. - -### 1.40.3 - -* Minor code clean-up with no functional effect. - -* (Issue #6) Removed non-project specific unwanted files out of `.gitignore`. These should be listed in the -developer's own global `~/.gitignore` file instead. - -### 1.40.2 - -* Added `getMapcodeFormatType` and `isValidMapcodeFormat` to check validity of mapcode strings. Added -unit tests for these methods as well. - -* Constructor of `Mapcode` now checks for validity of mapcode string. - -* Added Unicode handling of high precision mapcodes and added check to throw an `IllegalArgumentException` -if the character 'Z' or equivalent Unicode character is contained in the high precision part according to -the Mapcode documenation. - -* Added method `convertToAscii` which produces the ASCII, non-Unicode variant of a mapcode which contains -Unicode characters§. - -### 1.40.1 - -* Deprecated names `getMapcodeHighPrecision` and `getMapcodeMediumPrecision`. - Replaced those with `getMapcodePrecision1` and `getMapcodePrecision2`. - -* Fixed all occurences of incorrectly cased Mapcode vs. mapcode. - -### 1.40 - -* Renamed class `Mapcode` to `MapcodeCodec`. - -* Renamed class `MapcodeInfo` to `Mapcode`. - -* Added high precision Mapcodes, with methods `getMapcodeHighPrecision` - -* Seriously reduced test set size. - -* Replaced Unicode characters in source code to escapes. - -* Added explicit character encoding to `pom.xml`. - -* Fixed issues with decoder at some boundaries. - -### 1.33.2 - -* Clean-up of release 1.33.2. - -* Added release notes. - -* Removed GSON dependency from production (now scope 'test' only). - -* Added robustness with respect to Unicode characters. - -### 1.33.1 - -* First release of Java library for MapCodes. Includes extensive test suite. diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md deleted file mode 100644 index 0305f22..0000000 --- a/src/site/markdown/index.md +++ /dev/null @@ -1,216 +0,0 @@ -# Mapcode Library for Java - -Welcome to the Java library to handle mapcodes. The original C library was created by Pieter Geelen. -The initial port to Java and considerable speed-ups were done by Matthew Lowden. -Rijn Buve has further developed and maintained the Java version of the Mapcode library since. - -## Release Notes - -You can find the release notes for this library at: **[Release Notes](./ReleaseNotes.html)**. - -## Code Style Settings for IntelliJ IDEA - -The Java code uses the *default* [JetBrains IntelliJ IDEA](https://www.jetbrains.com/idea) -code style settings for Java, with one exception: -code blocks are always surround by `{...}` and on separate lines. - -### How To Build The Mapcode Library JAR File - -The sources for the Mapcode Library for Java contain everything to build the Mapcode JAR file as well -as a significant number of unit tests to verify the correctness of the implementation against the -reference C implementation. - -The library requires a minimum Java language revision level 6, but has been tested and verified to work -with JDK 1.6, JDK 1.7 and JDK 1.8. - -To build the library: - - cd - mvn clean install - -This produces a JAR file in your local Maven repository at `~/.m2/repository/com/mapcode/mapcode//` -You can include this JAR in your project, or store it in your local Nexus repository, for example. - -If you create a Maven project, you can see how to include the JAR in your project in the menu -item `Dependency Information` in the menu on this page. - -The latest official version of the libray on Maven Central can be found [**here**](http://search.maven.org/#search%7Cga%7C1%7Cmapcode). - -## How To Use This Library In Your Application - -There are two classes you interact with as a client of the Mapcode Library. These are: - - MapcodeCodec.java - Mapcode.java - Point.java - - -### Class `MapcodeCodec.java` - -This class contains the **encoder** and **decoder** for mapcodes. The class exposes two methods (with some -variants): `encode` and `decode`. - -The **encoder** encodes a (latitude, longitude) pair into a result set of mapcodes. A single (latitude, -longitude) pair may produce multiple mapcodes, some of which are known as **local** mapcodes -(which are only unique within a given territory) and one which is globally unique in the entire world. - -The **decoder** decodes a local or world-wide mapcode string into a (latitude, longitude) pair. For -local mapcodes a territory may be specified which is used to disambiguate the mapcode resolution. - -Note that encoding a (latitude, longitude) pair to a mapcode and then decoding it may result in a -slightly offset position, as the mapcodes have a limited precision. The library offers "high-precision" -mapcodes as well, but you van never assume the resulting latitudes and longitudes to exactly match the -original input. - -**`List encode(double latitude, double longitude)`** encodes a (latitude, longitude) pair. - -Example: - - double lat = 52.376514; - double lon = 4.908542; - List**Mapcode** results = MapcodeCodec.encode(lat, lon); - // Returns a non-empty list of results. - -This produces a non-empty list of resulting mapcodes. The shortest (potentially local) mapcodes is always -the first mapcodes in the list. The last mapcode in the list is always the -globally unique international mapcode. - -**`List encode(double latitude, double longitude, Territory territory)`** encodes a (latitude, -longitude) pair, where encoding is restricted to a specific territory. - -Example: - - List**Mapcode** results = MapcodeCodec.encode(lat, lon, Territory.NLD); - // Returns an empty list of results if the location is not within territory NLD. - -This resticts encoding to a specific territory and produces a potentially empty list of results. -Again, if encoding succeeded, the first mapcode is the shortest one and the last mapcode in the list is the -globally unique international mapcode. - -Both `encode()` methods are also offered as a `encodeToShortest()` method, which essentially -returns only the first result of the previous methods (if there are any results). - - Mapcode result = MapcodeCodec.encodeToShortest(lat, lon); - // Always returns a mapcode (or valid lat and lon values). - - try { - Mapcode result = MapcodeCodec.encodeToShortest(lat, lon, Territory.NLD); - // This may fail. - } - catch (UnknownMapcodeException e) { - // If the location is not within the territory, this exception is thrown. - } - -**`Point decode(String mapcode)`** decodes a mapcode to a `Point` which contains a location. Example: - - Point p = MapcodeCodec.decode("NLD 49.4V"); - -**`Point decode(String mapcode, Territory territory)`** decodes a mapcode to a `Point` which contains a -location, where the mapcode must be located within a specific territory. - -Examples of usage: - - Point p = MapcodeCodec.decode("49.4V", Territory.NLD); - - -## Class `Mapcode.java` - -This class represents mapcodes, which consist of a string of characters, digits and a decimal point and -a territory specification. The territory specification is required for national (local) mapcodes, which -are not globally unique. - -The class also exposes methods to convert mapcodes to proper mapcode strings, usable for printing and -it allows string-formatted mapcodes to be converted to `Mapcode` objects, where territory information -is properly parsed and converted to a `Territory` enumeration value. - -**`String getCode()`** returns the mapcode string which does not include territory information. You can also -use `getCode(1)` and `getCode(2)` for more precision, but longer mapcodes. - -The default precision offered by `getCode()` is approximately 10m (maximum distance to latitude, -longitude the mapcode decodes to). This corresponds to an area of 20m x 20m (400m2). These mapcodes include -no additional precision digits. - -The precision offered by `getCode(1)` is approximately 2m. -This corresponds to an area of 4m x 4m (16m2). These mapcodes include 1 additional precision digit. - -The precision offered by `getCode(2)` is approximately 0.40m. This corresponds to an area -of 0.80m x 0.80m (0.64m2). These mapcodes include 2 additional precision digits. - -This goes up to `getCode(8)`, which provides nanometer accuracy. (Please note one of the main advantages -of mapcodes over WGS84 coordinates is their simplicity and short size, so try to use as little precision -as required for your application...) - -**`Territory getTerritory()`** returns the territory information. - -**`toString()`** and **`getCodeWithTerritory()`** return mapcodes string with territory information, -specified as a ISO code. - - -## Class `Point.java` - -This class represents (latitude, longitude) locations. It offers methods to create locations using -degrees. - -**`Point fromDeg(double latitude, double longitude)`** returns a `Point` for a given (latitude, longitude) -pair. Note that latitudes are always between **-90** and **90**, and longitudes are -always between **-180** and **180** (non-inclusive) whenreturned. -However, values outside these range are correctly limited (latitude) or wrapped (longitude) to these ranges -when supplied to the class. - -The methods **`double getLat()`** and **`getLon()`** return the latitude and longitude respectively, in degrees. - - -# What Is A Mapcode? - -A mapcode represents a location. Every location on Earth can be represented by a mapcode. Mapcodes -were designed to be short, easy to recognise, remember and communicate. They are precise to a few -meters, which is good enough for every-day use. - -## Mapcodes Are Free! - -Mapcodes are free. They can be used by anyone, and may be supported, provided or generated by anyone, -as long as this is done free of charge, conditions or restrictions. Technical details and sources are -available on our developers page. - -## what Does A Mapcode Look Like? - -A mapcode consists of two groups of letters and digits, separated by a dot. An example of a mapcode is - - 49.4V - -This is sufficient as long as it is clear what country or state the mapcode belongs in. On a business card, -it is therefore a good idea to put it after the country or state name: - - John Smith - Oosterdoksstraat 114 - Amsterdam - Netherlands 49.4V - -When storing mapcodes in a database, it is recommended to explicitly specify the country: - - Netherlands 49.4V - -or via the standard 3-letter abbreviation: - - NLD 49.4V - -In eight very large countries (The USA, Canada, Mexico, Brazil, India, Australia, Russia, and China), -an address has little meaning without knowing the state (just like elsewhere, an address has little meaning -without knowing the country). For example, there are 27 cities called Washington in the USA. If you want to -refer to a location in the capital city, you would always refer to "Washington DC". - - DC 18.JQZ - -or (in an international database): - - US-DC 18.JQZ - -More information on mapcodes and their underlying concepts can be found in our reference material. - -## Where Did Mapcodes Come From? - -Mapcodes were developed in 2001 by Pieter Geelen and Harold Goddijn, soon after the GPS satellite signals -were opened up for civilian use. It was decided to donate the mapcode system to the public domain in 2008. -The algorithms and data tables are maintained by the Stichting Mapcode Foundation. - -The mapcode system is being filed as a standard at the International Organisation for Standardisation. diff --git a/src/site/site.xml b/src/site/site.xml deleted file mode 100644 index 4d9b53c..0000000 --- a/src/site/site.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - org.apache.maven.skins - maven-default-skin - 1.0 - - - - - - - - - - ${reports} - - diff --git a/src/test/java/com/mapcode/DecoderTest.java b/src/test/java/com/mapcode/DecoderTest.java index 031ae21..e8ad7d8 100644 --- a/src/test/java/com/mapcode/DecoderTest.java +++ b/src/test/java/com/mapcode/DecoderTest.java @@ -26,6 +26,64 @@ public class DecoderTest { private static final Logger LOG = LoggerFactory.getLogger(DecoderTest.class); + @SuppressWarnings("JUnitTestMethodWithNoAssertions") + @Test + public void getInternationalGrid() { + final DataModel dataModel = DataModel.getInstance(); + final int world = Territory.AAA.getNumber(); + final int to = dataModel.getDataLastRecord(world); + final int from = dataModel.getDataFirstRecord(world); + for (int index = from; index <= to; index++) { + final Boundary boundary = Boundary.createBoundaryForTerritoryRecord(index); + LOG.info("{}: ({}, {}), ({}, {})", (index - from) + 1, + boundary.getLatMicroDegMin() / 1.0e6, + boundary.getLonMicroDegMin() / 1.0e6, + boundary.getLatMicroDegMax() / 1.0e6, + boundary.getLonMicroDegMax() / 1.0e6 + ); + } + } + + @Test + public void decodeToRectangle() throws Exception { + LOG.info("decodeToRectangle"); + Point center = MapcodeCodec.decode("49.4V", Territory.NLD); + Rectangle rectangle = MapcodeCodec.decodeToRectangle("49.4V", Territory.NLD); + Point southWest = Point.fromDeg(52.37646900000124, 4.9084705); + Point northEast = Point.fromDeg(52.37655900000124, 4.908616250000001); + LOG.debug("decodeToRectangle: center={}. rectangle={}", center, rectangle); + assertEquals("center", center, rectangle.getCenter()); + assertEquals("southWest", southWest, rectangle.getSouthWest()); + assertEquals("northEast", northEast, rectangle.getNorthEast()); + + center = MapcodeCodec.decode("VHXGB.1J9J"); + rectangle = MapcodeCodec.decodeToRectangle("VHXGB.1J9J"); + southWest = Point.fromDeg(52.376483, 4.908505); + northEast = Point.fromDeg(52.376525, 4.908566); + LOG.debug("decodeToRectangle: center={}. rectangle={}", center, rectangle); + assertEquals("center", center, rectangle.getCenter()); + assertEquals("southWest", southWest, rectangle.getSouthWest()); + assertEquals("northEast", northEast, rectangle.getNorthEast()); + + center = MapcodeCodec.decode("VHXGB.ZPHH"); + rectangle = MapcodeCodec.decodeToRectangle("VHXGB.ZPHH"); + southWest = Point.fromDeg(52.370015, 4.963710); + northEast = Point.fromDeg(52.370057, 4.963758); + LOG.debug("decodeToRectangle: center={}. rectangle={}", center, rectangle); + assertEquals("center", center, rectangle.getCenter()); + assertEquals("southWest", southWest, rectangle.getSouthWest()); + assertEquals("northEast", northEast, rectangle.getNorthEast()); + + center = MapcodeCodec.decode("VHWK3.Z0HF"); + rectangle = MapcodeCodec.decodeToRectangle("VHWK3.Z0HF"); + southWest = Point.fromDeg(52.317869, 4.675245); + northEast = Point.fromDeg(52.317881, 4.675293); + LOG.debug("decodeToRectangle: center={}. rectangle={}", center, rectangle); + assertEquals("center", center, rectangle.getCenter()); + assertEquals("southWest", southWest, rectangle.getSouthWest()); + assertEquals("northEast", northEast, rectangle.getNorthEast()); + } + @Test public void decodeMapcodeWithTerritory() throws Exception { LOG.info("decodeMapcodeWithTerritory"); diff --git a/src/test/java/com/mapcode/TerritoryTest.java b/src/test/java/com/mapcode/TerritoryTest.java index aeec034..6b406d4 100644 --- a/src/test/java/com/mapcode/TerritoryTest.java +++ b/src/test/java/com/mapcode/TerritoryTest.java @@ -29,8 +29,8 @@ public class TerritoryTest { private static final Logger LOG = LoggerFactory.getLogger(TerritoryTest.class); @Test(expected = UnknownTerritoryException.class) - public void emptyTerritoryCodeTest() { - LOG.info("emptyCodeTest"); + public void emptyTerritoryNumberTest() { + LOG.info("emptyTerritoryNumberTest"); Territory.fromString(""); }