Encoder.java

/*
 * Copyright (C) 2014-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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;

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.
 */
@SuppressWarnings({"MagicNumber", "StringConcatenationMissingWhitespace"})
final class Encoder {
    private static final Logger LOG = LoggerFactory.getLogger(Encoder.class);

    // Get direct access to data model singleton.
    private static final DataModel DATA_MODEL = DataModel.getInstance();

    private Encoder() {
        // Prevent instantiation.
    }

    // ----------------------------------------------------------------------
    // Method called from public Java API.
    // ----------------------------------------------------------------------

    @Nonnull
    static List<Mapcode> encode(
            final double latDeg,
            final double lonDeg,
            @Nullable final Territory territory,
            final boolean limitToOneResult) {

        return encode(latDeg, lonDeg, territory, limitToOneResult, null);
    }

    // ----------------------------------------------------------------------
    // Private methods.
    // ----------------------------------------------------------------------

    private final static char[] ENCODE_CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F',
            'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'A', 'E', 'U'};

    @Nonnull
    private static List<Mapcode> 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<Mapcode> results = new ArrayList<Mapcode>();
        int lastBaseSubTerritoryNumber = -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++) {

            // 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 lastSubTerritoryRecord = DATA_MODEL.getDataFirstRecord(territoryRecord);
                final Territory currentEncodeTerritory = Territory.fromNumber(territoryRecord);

                for (int subTerritoryRecord = lastSubTerritoryRecord; subTerritoryRecord <= firstSubTerritoryRecord; subTerritoryRecord++) {

                    // Check if the point to encode is contained within the boundary.
                    if (createBoundaryForTerritoryRecord(subTerritoryRecord).containsPoint(pointToEncode)) {

                        // All fine, proceed with creating a mapcode.
                        String mapcode = "";
                        if (Data.isNameless(subTerritoryRecord)) {
                            mapcode = encodeNameless(pointToEncode, subTerritoryRecord, lastSubTerritoryRecord);

                        } 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);
                            }
                        } else {
                            // Skip this record.
                        }

                        // 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)) {
                                LOG.error("encode: Duplicate results found, newResult={}, results={} items",
                                        newResult.getCodeWithTerritory(), results.size());
                            } else {

                                // Remove existing results (if there was a parent territory).
                                if (limitToOneResult) {
                                    results.clear();
                                }
                                results.add(newResult);
                            }

                            lastBaseSubTerritoryNumber = lastSubTerritoryRecord;

                            // Stop if we only need a single result anyway.
                            if (limitToOneResult) {
                                return results;
                            }
                        }
                    }
                }
            }
        }
        LOG.trace("encode: results={} items", results.size());
        LOG.trace("");
        return results;
    }

    @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;
        double factory = Point.MAX_PRECISION_FACTOR * dividery;
        double valx = (Point.MAX_PRECISION_FACTOR * extrax4) + pointToEncode.getLonFraction();
        double valy = (Point.MAX_PRECISION_FACTOR * extray) + (ydirection * pointToEncode.getLatFraction());

        final StringBuilder sb = new StringBuilder();
        sb.append('-');

        while (true) {
            factorx /= 30;
            //noinspection NumericCastThatLosesPrecision
            final int gx = (int) (valx / factorx);

            factory /= 30;
            //noinspection NumericCastThatLosesPrecision
            final int gy = (int) (valy / factory);

            sb.append(ENCODE_CHARS[((gy / 5) * 5) + (gx / 6)]);
            --extraDigits;
            if (extraDigits == 0) {
                break;
            }

            sb.append(ENCODE_CHARS[(((gy % 5) * 6) + (gx % 6))]);
            --extraDigits;
            if (extraDigits == 0) {
                break;
            }

            valx -= factorx * gx;
            valy -= factory * gy;
        }
        return sb.toString();
    }

    @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;
        } else if (codexm == 14) {
            codexm = 23;
        }
        final int prelen = codexm / 10;
        final int postlen = codexm % 10;
        final int divx;
        int divy = DATA_MODEL.getSmartDiv(territoryNumber);
        if (divy == 1) {
            divx = X_SIDE[prelen];
            divy = Y_SIDE[prelen];
        } else {
            divx = NC[prelen] / divy;
        }

        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;
        rely = rely / ygridsize;

        final int xgridsize = (((maxx - minx) + divx) - 1) / divx;
        int x = pointToEncode.getLonMicroDeg();
        int relx = x - minx;
        if (relx < 0) {
            x += Point.MICRO_DEG_360;
            relx += Point.MICRO_DEG_360;
        } else if (relx >= Point.MICRO_DEG_360) {
            x -= Point.MICRO_DEG_360;
            relx -= Point.MICRO_DEG_360;
        }
        if (relx < 0) {
            return "";
        }
        relx = relx / xgridsize;
        if ((relx >= divx) || (rely >= divy)) {
            return "";
        }

        final int v;
        if ((divx != divy) && (prelen > 2)) {
            v = encodeSixWide(relx, rely, divx, divy);
        } else {
            v = ((relx * divy) + divy) - 1 - rely;
        }

        String result = encodeBase31(v, prelen);

        if ((prelen == 4) && (divx == 961) && (divy == 961)) {
            result = String.valueOf(result.charAt(0)) + result.charAt(2) + result.charAt(1) + result.charAt(3);
        }

        rely = miny + (rely * ygridsize);
        relx = minx + (relx * xgridsize);

        final int dividery = ((ygridsize + Y_SIDE[postlen]) - 1) / Y_SIDE[postlen];
        final int dividerx = ((xgridsize + X_SIDE[postlen]) - 1) / X_SIDE[postlen];

        result += '.';

        int difx = x - relx;
        int dify = pointToEncode.getLatMicroDeg() - rely;

        final int extrax = difx % dividerx;
        final int extray = dify % dividery;

        difx = difx / dividerx;
        dify = dify / dividery;

        dify = Y_SIDE[postlen] - 1 - dify;
        if (postlen == 3) {
            result += encodeTriple(difx, dify);
        } else {

            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);
            }
            result += postfix;
        }

        if (orgcodex == 14) {
            result = result.charAt(0) + "." + result.charAt(1) + result.substring(3);
        }

        result += encodeExtension(pointToEncode, extrax << 2, extray, dividerx << 2, dividery, 1); // grid

        return Data.headerLetter(territoryNumber) + result;
    }

    @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 = territoryRecord;
        while ((Data.getTerritoryRecordType(firstindex - 1) > Data.TERRITORY_RECORD_TYPE_PIPE) && (Data.getCodex(firstindex - 1) == codexm)) {
            firstindex--;
        }

        int i = firstindex;
        while (true) {

            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);
            int w = ((((maxx - minx) * 4) + xdiv) - 1) / xdiv;

            h = 176 * (((h + 176) - 1) / 176);
            w = 168 * (((w + 168) - 1) / 168);

            int product = (w / 168) * (h / 176) * 961 * 31;

            if (Data.getTerritoryRecordType(i) == Data.TERRITORY_RECORD_TYPE_PLUS) // plus pipe
            {
                final int goodRounder = (codexm >= 23) ? (961 * 961 * 31) : (961 * 961);
                product =
                        ((((storageStart + product + goodRounder) - 1) / goodRounder) * goodRounder) - storageStart;
            }

            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;

                final int dividery = (((maxy - miny) + h) - 1) / h;
                int vy = (maxy - pointToEncode.getLatMicroDeg()) / dividery;
                int extray = (maxy - pointToEncode.getLatMicroDeg()) % dividery;

                int value = (vx / 168) * (h / 176);
                if ((extray == 0) && (pointToEncode.getLatFraction() > 0)) {
                    vy--;
                    extray += dividery;
                }
                value += (vy / 176);

                final int codexlen = (codexm / 10) + (codexm % 10);
                stringBuilder.append(encodeBase31((storageStart / (961 * 31)) + value, codexlen - 2));
                stringBuilder.append('.');
                stringBuilder.append(encodeTriple(vx % 168, vy % 176));

                stringBuilder.append(
                        encodeExtension(pointToEncode, extrax << 2, extray, dividerx << 2, dividery, -1)); // AutoHeader
                return stringBuilder.toString();
            }

            storageStart += product;
            i++;
        }
    }

    @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 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 = territoryRecord - firstNamelessRecord;

        int storage_offset;

        if ((codexm != 21) && (a <= 31)) {
            storage_offset = ((nrX * p) + ((nrX < r) ? nrX : r)) * (961 * 961);
        } else if ((codexm != 21) && (a < 62)) {
            if (nrX < (62 - a)) {
                storage_offset = nrX * 961 * 961;
            } else {
                storage_offset = ((62 - a) + (((nrX - 62) + a) / 2)) * 961 * 961;
                if (((nrX + a) & 1) != 0) {
                    storage_offset += 16 * 961 * 31;
                }
            }
        } else {
            final int basePower = (codexm == 21) ? (961 * 961) : (961 * 961 * 31);
            int basePowerA = basePower / a;
            if (a == 62) {
                basePowerA++;
            } else {
                basePowerA = 961 * (basePowerA / 961);
            }

            storage_offset = nrX * basePowerA;
        }

        int side = DATA_MODEL.getSmartDiv(territoryRecord);
        final int orgSide = side;
        int xSide = side;

        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;
        final int dminx = pointToEncode.getLonMicroDeg() - minx;
        final int dx = ((4 * dminx) + xFracture) / dividerx4;
        final int extrax4 = (4 * dminx) - (dx * dividerx4); // like modulus, but with floating point value

        final int dividery = 90;
        final int dmaxy = maxy - pointToEncode.getLatMicroDeg();
        int dy = dmaxy / dividery;
        int extray = dmaxy % dividery;

        if ((extray == 0) && (pointToEncode.getLatFraction() > 0)) {
            dy--;
            extray += dividery;
        }

        int v = storage_offset;
        if (Data.isSpecialShape(territoryRecord)) {
            xSide *= side;
            side = 1 + ((maxy - miny) / 90);
            xSide = xSide / side;
            v += encodeSixWide(dx, side - 1 - dy, xSide, side);
        } else {
            v += (dx * side) + dy;
        }

        String result = encodeBase31(v, codexlen + 1);

        if (codexlen == 3) {
            result = result.substring(0, 2) + '.' + result.substring(2);
        } else if (codexlen == 4) {
            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) {
                result = result.substring(0, 2) + '.' + result.substring(2);
            } else {
                result = result.substring(0, 3) + '.' + result.substring(3);
            }
        }
        result += encodeExtension(pointToEncode, extrax4, extray, dividerx4, dividery, -1); // for encodeNameless

        return result;
    }

    @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();
        int d;
        String rest = "";
        for (d = 0; d < rlen; d++) {
            if ((str.charAt(d) < '0') || (str.charAt(d) > '9')) // not digit?
            {
                if ((str.charAt(d) == '.') && (dotpos < 0)) // first dot?
                {
                    dotpos = d;
                } else if (str.charAt(d) == '-') {
                    rest = str.substring(d);
                    str = str.substring(0, d);
                    rlen = d;
                } else {
                    return str; // not all-digit (or multiple dots)
                }
            }
        }

        if ((rlen - 2) > dotpos) {
            // does r have a dot, AND at least 2 chars after the dot?
            if (argShort) { // use A only
                final int v = ((((int) str.charAt(0)) - 48) * 100) + ((((int) str.charAt(rlen - 2)) - 48) * 10) + (((int) str.charAt(rlen - 1)) - 48);
                return 'A' + str.substring(1, rlen - 2) + ENCODE_CHARS[v / 32] + ENCODE_CHARS[v % 32] + rest;
            } else { // use A, E and U
                final int v = (((((int) str.charAt(rlen - 2)) - 48) * 10) + ((int) str.charAt(rlen - 1))) - 48;
                str = str.substring(0, rlen - 2) + ENCODE_CHARS[31 + (v / 34)] + ENCODE_CHARS[v % 34];
            }
        }
        return str + rest;
    }

    @Nonnull
    private static String encodeBase31(
            final int argValue,
            final int argNrChars) {
        int value = argValue;
        int nrChars = argNrChars;
        final StringBuilder result = new StringBuilder();
        while (nrChars > 0) {
            nrChars--;
            result.insert(0, ENCODE_CHARS[value % 31]);
            value = value / 31;
        }
        return result.toString();
    }

    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) {
            col = maxcol;
            d = width - (maxcol * 6);
        } else {
            d = 6;
        }
        return ((height * 6 * col) + ((height - 1 - y) * d) + x) - (col * 6);
    }

    @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 {
            return ENCODE_CHARS[((difx / 24) + 24)] + encodeBase31((((difx % 24) * 40) + dify) - 136, 2);
        }
    }
}