Decoder.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 static com.mapcode.Boundary.createBoundaryForTerritoryRecord;
// ----------------------------------------------------------------------------------------------
// Package private implementation class. For internal use within the mapcode implementation only.
//----------------------------------------------------------------------------------------------
/**
* This class contains decoder for mapcodes.
*/
@SuppressWarnings({"MagicNumber", "StringConcatenationMissingWhitespace"})
final class Decoder {
private static final Logger LOG = LoggerFactory.getLogger(Decoder.class);
// Get direct access to the data model singleton.
private static final DataModel DATA_MODEL = DataModel.getInstance();
private Decoder() {
// Prevent instantiation.
}
// ----------------------------------------------------------------------
// Method called from public Java API.
// ----------------------------------------------------------------------
@Nonnull
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 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);
}
// 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()) {
// 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 codexLen = mapcode.length() - 1;
// *** long codes in states are handled by the country
if (codexLen >= 9) {
// International codes are 9 characters.
assert codexLen == 9;
territory = Territory.AAA;
} else {
final Territory parentTerritory = territory.getParentTerritory();
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 fromTerritoryRecord = DATA_MODEL.getDataFirstRecord(territoryNumber);
final int uptoTerritoryRecord = DATA_MODEL.getDataLastRecord(territoryNumber);
// 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 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 (((codexOfTerritory == 21) && (codex == 22)) ||
((codexOfTerritory == 22) && (codex == 32)) ||
((codexOfTerritory == 13) && (codex == 23))) {
mapcodeZone = decodeNameless(mapcode, territoryRecord, precisionPostfix);
break;
}
} else {
// i = grid without headerletter
if ((codexOfTerritory == codex) ||
((codex == 22) && (codexOfTerritory == 21))) {
mapcodeZone = decodeGrid(mapcode,
boundaryOfTerritory.getLonMicroDegMin(), boundaryOfTerritory.getLatMicroDegMin(),
boundaryOfTerritory.getLonMicroDegMax(), boundaryOfTerritory.getLatMicroDegMax(),
territoryRecord, precisionPostfix);
// first of all, make sure the zone fits the country
mapcodeZone = mapcodeZone.restrictZoneTo(createBoundaryForTerritoryRecord(uptoTerritoryRecord));
if (Data.isRestricted(territoryRecord) && !mapcodeZone.isEmpty()) {
int nrZoneOverlaps = 0;
int j;
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 (createBoundaryForTerritoryRecord(j).containsPoint(result)) {
nrZoneOverlaps++;
break;
}
}
}
if (nrZoneOverlaps == 0) {
// see if mapcode zone OVERLAPS any sub-area...
MapcodeZone zfound = new MapcodeZone();
for (j = fromTerritoryRecord; j < territoryRecord; j++) { // try all smaller rectangles j
if (!Data.isRestricted(j)) {
final MapcodeZone z = mapcodeZone.restrictZoneTo(createBoundaryForTerritoryRecord(j));
if (!z.isEmpty()) {
nrZoneOverlaps++;
if (nrZoneOverlaps == 1) {
// first fit! remember...
zfound = new MapcodeZone(z);
} else { // nrZoneOverlaps > 1
// more than one hit
break; // give up!
}
}
}
}
if (nrZoneOverlaps == 1) { // intersected exactly ONE sub-area?
mapcodeZone = new MapcodeZone(zfound); // use the intersection found...
}
}
if (nrZoneOverlaps == 0) {
mapcodeZone = new MapcodeZone();
}
}
break;
}
}
} else if (Data.getTerritoryRecordType(territoryRecord) == Data.TERRITORY_RECORD_TYPE_PIPE) {
// i = grid with headerletter
if ((codex == (codexOfTerritory + 10)) &&
(Data.headerLetter(territoryRecord).charAt(0) == mapcode.charAt(0))) {
mapcodeZone = decodeGrid(mapcode.substring(1),
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 (((codex == 23) && (codexOfTerritory == 22)) ||
((codex == 33) && (codexOfTerritory == 23))) {
mapcodeZone = decodeAutoHeader(mapcode, territoryRecord, precisionPostfix);
break;
}
}
}
mapcodeZone = mapcodeZone.restrictZoneTo(createBoundaryForTerritoryRecord(uptoTerritoryRecord));
LOG.trace("decode: zone={}", mapcodeZone);
return mapcodeZone;
}
// ----------------------------------------------------------------------
// Private methods.
// ----------------------------------------------------------------------
private final static int[] DECODE_CHARS = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1,
-1, -2, 10, 11, 12, -3, 13, 14, 15, 1, 16, 17, 18, 19, 20, 0,
21, 22, 23, 24, 25, -4, 26, 27, 28, 29, 30, -1, -1, -1, -1, -1,
-1, -2, 10, 11, 12, -3, 13, 14, 15, 1, 16, 17, 18, 19, 20, 0,
21, 22, 23, 24, 25, -4, 26, 27, 28, 29, 30, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
};
private static class Unicode2Ascii {
final char min;
final char max;
@Nonnull
final String convert;
Unicode2Ascii(final char min, final char max, @Nonnull final String convert) {
this.min = min;
this.max = max;
this.convert = convert;
}
}
// Greek character A.
private static final char GREEK_CAPITAL_ALPHA = '\u0391';
// Special character '?' indicating missing character in alphabet.
private static final char MISSCODE = '?';
// @formatter:off
@SuppressWarnings("LongLine") private final static char[][] ASCII2LANGUAGE = {
// Character: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9
/* Roman */ {'\u0041', '\u0042', '\u0043', '\u0044', '\u0045', '\u0046', '\u0047', '\u0048', '\u0049', '\u004a', '\u004b', '\u004c', '\u004d', '\u004e', '\u004f', '\u0050', '\u0051', '\u0052', '\u0053', '\u0054', '\u0055', '\u0056', '\u0057', '\u0058', '\u0059', '\u005a', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Roman
/* Greek */ {'\u0391', '\u0392', '\u039e', '\u0394', '\u0388', '\u0395', '\u0393', '\u0397', '\u0399', '\u03a0', '\u039a', '\u039b', '\u039c', '\u039d', '\u039f', '\u03a1', '\u0398', '\u03a8', '\u03a3', '\u03a4', '\u0389', '\u03a6', '\u03a9', '\u03a7', '\u03a5', '\u0396', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Greek
/* Cyrillic */ {'\u0410', '\u0412', '\u0421', '\u0414', '\u0415', '\u0416', '\u0413', '\u041d', '\u0418', '\u041f', '\u041a', '\u041b', '\u041c', '\u0417', '\u041e', '\u0420', '\u0424', '\u042f', '\u0426', '\u0422', '\u042d', '\u0427', '\u0428', '\u0425', '\u0423', '\u0411', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Cyrillic
/* Hebrew */ {'\u05d0', '\u05d1', '\u05d2', '\u05d3', '\u05e3', '\u05d4', '\u05d6', '\u05d7', '\u05d5', '\u05d8', '\u05d9', '\u05da', '\u05db', '\u05dc', '\u05e1', '\u05dd', '\u05de', '\u05e0', '\u05e2', '\u05e4', '\u05e5', '\u05e6', '\u05e7', '\u05e8', '\u05e9', '\u05ea', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Hebrew
/* Devanag. */ {'\u0905', '\u0915', '\u0917', '\u0918', '\u090f', '\u091a', '\u091c', '\u091f', MISSCODE, '\u0920', '\u0923', '\u0924', '\u0926', '\u0927', MISSCODE, '\u0928', '\u092a', '\u092d', '\u092e', '\u0930', '\u092b', '\u0932', '\u0935', '\u0938', '\u0939', '\u092c', '\u0966', '\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e', '\u096f'}, // Devanagiri
/* Malay */ {'\u0d12', '\u0d15', '\u0d16', '\u0d17', '\u0d0b', '\u0d1a', '\u0d1c', '\u0d1f', '\u0d07', '\u0d21', '\u0d24', '\u0d25', '\u0d26', '\u0d27', '\u0d20', '\u0d28', '\u0d2e', '\u0d30', '\u0d31', '\u0d32', '\u0d09', '\u0d34', '\u0d35', '\u0d36', '\u0d38', '\u0d39', '\u0d66', '\u0d67', '\u0d68', '\u0d69', '\u0d6a', '\u0d6b', '\u0d6c', '\u0d6d', '\u0d6e', '\u0d6f'}, // Malay
/* Georgian */ {'\u10a0', '\u10a1', '\u10a3', '\u10a6', '\u10a4', '\u10a9', '\u10ab', '\u10ac', '\u10b3', '\u10ae', '\u10b0', '\u10b1', '\u10b2', '\u10b4', '\u10ad', '\u10b5', '\u10b6', '\u10b7', '\u10b8', '\u10b9', '\u10a8', '\u10ba', '\u10bb', '\u10bd', '\u10be', '\u10bf', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Georgian
/* Katakana */ {'\u30a2', '\u30ab', '\u30ad', '\u30af', '\u30aa', '\u30b1', '\u30b3', '\u30b5', '\u30a4', '\u30b9', '\u30c1', '\u30c8', '\u30ca', '\u30cc', '\u30a6', '\u30d2', '\u30d5', '\u30d8', '\u30db', '\u30e1', '\u30a8', '\u30e2', '\u30e8', '\u30e9', '\u30ed', '\u30f2', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Katakana
/* Thai */ {'\u0e30', '\u0e01', '\u0e02', '\u0e04', '\u0e32', '\u0e07', '\u0e08', '\u0e09', '\u0e31', '\u0e0a', '\u0e11', '\u0e14', '\u0e16', '\u0e17', '\u0e0d', '\u0e18', '\u0e1a', '\u0e1c', '\u0e21', '\u0e23', '\u0e2c', '\u0e25', '\u0e27', '\u0e2d', '\u0e2e', '\u0e2f', '\u0e50', '\u0e51', '\u0e52', '\u0e53', '\u0e54', '\u0e55', '\u0e56', '\u0e57', '\u0e58', '\u0e59'}, // Thai
/* Laos */ {'\u0eb0', '\u0e81', '\u0e82', '\u0e84', '\u0ec3', '\u0e87', '\u0e88', '\u0e8a', '\u0ec4', '\u0e8d', '\u0e94', '\u0e97', '\u0e99', '\u0e9a', '\u0ec6', '\u0e9c', '\u0e9e', '\u0ea1', '\u0ea2', '\u0ea3', '\u0ebd', '\u0ea7', '\u0eaa', '\u0eab', '\u0ead', '\u0eaf', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Laos
/* Armenian */ {'\u0556', '\u0532', '\u0533', '\u0534', '\u0535', '\u0538', '\u0539', '\u053a', '\u053b', '\u053d', '\u053f', '\u0540', '\u0541', '\u0543', '\u0555', '\u0547', '\u0548', '\u054a', '\u054d', '\u054e', '\u0545', '\u054f', '\u0550', '\u0551', '\u0552', '\u0553', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Armenian
/* Bengali */ {'\u099c', '\u0998', '\u0995', '\u0996', '\u09ae', '\u0997', '\u0999', '\u099a', '\u09ab', '\u099d', '\u09a0', '\u09a1', '\u09a2', '\u09a3', '\u099e', '\u09a4', '\u09a5', '\u09a6', '\u09a8', '\u09aa', '\u099f', '\u09ac', '\u09ad', '\u09af', '\u09b2', '\u09b9', '\u09e6', '\u09e7', '\u09e8', '\u09e9', '\u09ea', '\u09eb', '\u09ec', '\u09ed', '\u09ee', '\u09ef'}, // Bengali/Assamese
/* Gurmukhi */ {'\u0a05', '\u0a15', '\u0a17', '\u0a18', '\u0a0f', '\u0a1a', '\u0a1c', '\u0a1f', MISSCODE, '\u0a20', '\u0a23', '\u0a24', '\u0a26', '\u0a27', MISSCODE, '\u0a28', '\u0a2a', '\u0a2d', '\u0a2e', '\u0a30', '\u0a2b', '\u0a32', '\u0a35', '\u0a38', '\u0a39', '\u0a21', '\u0a66', '\u0a67', '\u0a68', '\u0a69', '\u0a6a', '\u0a6b', '\u0a6c', '\u0a6d', '\u0a6e', '\u0a6f'}, // Gurmukhi
/* Tibetan */ {'\u0f58', '\u0f40', '\u0f41', '\u0f42', '\u0f64', '\u0f44', '\u0f45', '\u0f46', MISSCODE, '\u0f47', '\u0f49', '\u0f55', '\u0f50', '\u0f4f', MISSCODE, '\u0f51', '\u0f53', '\u0f54', '\u0f56', '\u0f5e', '\u0f60', '\u0f5f', '\u0f61', '\u0f62', '\u0f63', '\u0f66', '\u0f20', '\u0f21', '\u0f22', '\u0f23', '\u0f24', '\u0f25', '\u0f26', '\u0f27', '\u0f28', '\u0f29'}, // Tibetan
/* Arabic */ {'\u0628', '\u062a', '\u062d', '\u062e', '\u062B', '\u062f', '\u0630', '\u0631', '\u0627', '\u0632', '\u0633', '\u0634', '\u0635', '\u0636', '\u0647', '\u0637', '\u0638', '\u0639', '\u063a', '\u0641', '\u0642', '\u062C', '\u0644', '\u0645', '\u0646', '\u0648', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Arabic
/* Korean */ {'\u1112', '\u1100', '\u1102', '\u1103', '\u1166', '\u1105', '\u1107', '\u1109', '\u1175', '\u1110', '\u1111', '\u1161', '\u1162', '\u1163', '\u110b', '\u1164', '\u1165', '\u1167', '\u1169', '\u1172', '\u1174', '\u110c', '\u110e', '\u110f', '\u116d', '\u116e', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Korean
/* Burmese */ {'\u1005', '\u1000', '\u1001', '\u1002', '\u1013', '\u1003', '\u1004', '\u101a', '\u101b', '\u1007', '\u100c', '\u100d', '\u100e', '\u1010', '\u101d', '\u1011', '\u1012', '\u101e', '\u1014', '\u1015', '\u1016', '\u101f', '\u1017', '\u1018', '\u100f', '\u101c', '\u1040', '\u1041', '\u1042', '\u1043', '\u1044', '\u1045', '\u1046', '\u1047', '\u1048', '\u1049'}, // Burmese
/* Khmer */ {'\u1789', '\u1780', '\u1781', '\u1782', '\u1785', '\u1783', '\u1784', '\u1787', '\u179a', '\u1788', '\u178a', '\u178c', '\u178d', '\u178e', '\u179c', '\u1791', '\u1792', '\u1793', '\u1794', '\u1795', '\u179f', '\u1796', '\u1798', '\u179b', '\u17a0', '\u17a2', '\u17e0', '\u17e1', '\u17e2', '\u17e3', '\u17e4', '\u17e5', '\u17e6', '\u17e7', '\u17e8', '\u17e9'}, // Khmer
/* Sinhalese*/ {'\u0d85', '\u0d9a', '\u0d9c', '\u0d9f', '\u0d89', '\u0da2', '\u0da7', '\u0da9', '\u0dc2', '\u0dac', '\u0dad', '\u0daf', '\u0db1', '\u0db3', '\u0dc5', '\u0db4', '\u0db6', '\u0db8', '\u0db9', '\u0dba', '\u0d8b', '\u0dbb', '\u0dbd', '\u0dc0', '\u0dc3', '\u0dc4', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Sinhalese
/* Thaana */ {'\u0794', '\u0780', '\u0781', '\u0782', '\u0797', '\u0783', '\u0784', '\u0785', '\u07a4', '\u0786', '\u0787', '\u0788', '\u0789', '\u078a', '\u0796', '\u078b', '\u078c', '\u078d', '\u078e', '\u078f', '\u079c', '\u0790', '\u0791', '\u0792', '\u0793', '\u07b1', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Thaana
/* Chinese */ {'\u3123', '\u3105', '\u3108', '\u3106', '\u3114', '\u3107', '\u3109', '\u310a', '\u311e', '\u310b', '\u310c', '\u310d', '\u310e', '\u310f', '\u3120', '\u3115', '\u3116', '\u3110', '\u3111', '\u3112', '\u3113', '\u3129', '\u3117', '\u3128', '\u3118', '\u3119', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Chinese
/* Tifinagh */ {'\u2D49', '\u2D31', '\u2D33', '\u2D37', '\u2D53', '\u2D3C', '\u2D3D', '\u2D40', '\u2D4F', '\u2D43', '\u2D44', '\u2D45', '\u2D47', '\u2D4D', '\u2D54', '\u2D4E', '\u2D55', '\u2D56', '\u2D59', '\u2D5A', '\u2D62', '\u2D5B', '\u2D5C', '\u2D5F', '\u2D61', '\u2D63', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Tifinagh (BERBER)
/* Tamil */ {'\u0b99', '\u0b95', '\u0b9a', '\u0b9f', '\u0b86', '\u0ba4', '\u0ba8', '\u0baa', '\u0ba9', '\u0bae', '\u0baf', '\u0bb0', '\u0bb2', '\u0bb5', '\u0b9e', '\u0bb4', '\u0bb3', '\u0bb1', '\u0b85', '\u0b88', '\u0b93', '\u0b89', '\u0b8e', '\u0b8f', '\u0b90', '\u0b92', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Tamil (digits 0xBE6-0xBEF)
/* Amharic */ {'\u121B', '\u1260', '\u1264', '\u12F0', '\u121E', '\u134A', '\u1308', '\u1200', '\u12A0', '\u12E8', '\u12AC', '\u1208', '\u1293', '\u1350', '\u12D0', '\u1354', '\u1240', '\u1244', '\u122C', '\u1220', '\u12C8', '\u1226', '\u1270', '\u1276', '\u1338', '\u12DC', '\u1372', '\u1369', '\u136a', '\u136b', '\u136c', '\u136d', '\u136e', '\u136f', '\u1370', '\u1371'}, // Amharic (digits 1372|1369-1371)
/* Telugu */ {'\u0C1E', '\u0C15', '\u0C17', '\u0C19', '\u0C2B', '\u0C1A', '\u0C1C', '\u0C1F', '\u0C1B', '\u0C20', '\u0C21', '\u0C23', '\u0C24', '\u0C25', '\u0C16', '\u0C26', '\u0C27', '\u0C28', '\u0C2A', '\u0C2C', '\u0C2D', '\u0C2E', '\u0C30', '\u0C32', '\u0C33', '\u0C35', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Telugu
/* Odia */ {'\u0B1D', '\u0B15', '\u0B16', '\u0B17', '\u0B23', '\u0B18', '\u0B1A', '\u0B1C', '\u0B2B', '\u0B1F', '\u0B21', '\u0B22', '\u0B24', '\u0B25', '\u0B20', '\u0B26', '\u0B27', '\u0B28', '\u0B2A', '\u0B2C', '\u0B39', '\u0B2E', '\u0B2F', '\u0B30', '\u0B33', '\u0B38', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Odia
/* Kannada */ {'\u0C92', '\u0C95', '\u0C96', '\u0C97', '\u0C8E', '\u0C99', '\u0C9A', '\u0C9B', '\u0C85', '\u0C9C', '\u0CA0', '\u0CA1', '\u0CA3', '\u0CA4', '\u0C89', '\u0CA6', '\u0CA7', '\u0CA8', '\u0CAA', '\u0CAB', '\u0C87', '\u0CAC', '\u0CAD', '\u0CB0', '\u0CB2', '\u0CB5', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'}, // Kannada
/* Gujarati */ {'\u0AB3', '\u0A97', '\u0A9C', '\u0AA1', '\u0A87', '\u0AA6', '\u0AAC', '\u0A95', '\u0A8F', '\u0A9A', '\u0A9F', '\u0AA4', '\u0AAA', '\u0AA0', '\u0A8D', '\u0AB0', '\u0AB5', '\u0A9E', '\u0AAE', '\u0AAB', '\u0A89', '\u0AB7', '\u0AA8', '\u0A9D', '\u0AA2', '\u0AAD', '\u0030', '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0038', '\u0039'} // Gujarati
};
// @formatter:on
// @formatter:off
@SuppressWarnings("LongLine") private final static Unicode2Ascii[] UNICODE2ASCII = {
/* Roman */ new Unicode2Ascii('\u0041', '\u005a', "ABCDEFGHIJKLMNOPQRSTUVWXYZ"), // Roman
/* Greek */ new Unicode2Ascii('\u0388', '\u03a9', "EU???????ABGDFZHQIKLMNCOJP?STYVXRW"), // Greek
/* Cyrillic */ new Unicode2Ascii('\u0410', '\u042f', "AZBGDEFNI?KLMHOJPCTYQXSVW????U?R"), // Cyrillic
/* Hebrew */ new Unicode2Ascii('\u05d0', '\u05ea', "ABCDFIGHJKLMNPQ?ROSETUVWXYZ"), // Hebrew
/* Devanag. */ new Unicode2Ascii('\u0905', '\u0939', "A?????????E?????B?CD?F?G??HJZ?KL?MNP?QUZRS?T?V??W??XY"), // Devanagiri
/* Malay */ new Unicode2Ascii('\u0d07', '\u0d39', "I?U?E??????A??BCD??F?G??HOJ??KLMNP?????Q?RST?VWX?YZ"), // Malai
/* Georgian */ new Unicode2Ascii('\u10a0', '\u10bf', "AB?CE?D?UF?GHOJ?KLMINPQRSTVW?XYZ"), // Georgian
/* Katakana */ new Unicode2Ascii('\u30a2', '\u30f2', "A?I?O?U?EB?C?D?F?G?H???J???????K??????L?M?N?????P??Q??R??S?????TV?????WX???Y????Z"), // Katakana
/* Thai */ new Unicode2Ascii('\u0e01', '\u0e32', "BC?D??FGHJ??O???K??L?MNP?Q?R????S?T?V?W????UXYZAIE"), // Thai
/* Laos */ new Unicode2Ascii('\u0e81', '\u0ec6', "BC?D??FG?H??J??????K??L?MN?P?Q??RST???V??WX?Y?ZA????????????U?????EI?O"), // Lao
/* Armenian */ new Unicode2Ascii('\u0532', '\u0556', "BCDE??FGHI?J?KLM?N?U?PQ?R??STVWXYZ?OA"), // Armenian
/* Bengali */ new Unicode2Ascii('\u0995', '\u09b9', "CDFBGH?AJOUKLMNPQR?S?TIVWEX??Y??????Z"), // Bengali/Assamese
/* Gurmukhi */ new Unicode2Ascii('\u0a05', '\u0a39', "A?????????E?????B?CD?F?G??HJZ?KL?MNP?QU?RS?T?V??W??XY"), // Gurmukhi
/* Tibetan */ new Unicode2Ascii('\u0f40', '\u0f66', "BCD?FGHJ?K?????NMP?QRLS?A?????TVUWXYE?Z"), // Tibetan
/* Arabic */ new Unicode2Ascii('\u0627', '\u0648', "IA?BEVCDFGHJKLMNPQRS??????TU?WXYOZ"), // Arabic
/* Devanag. */ new Unicode2Ascii('\u0966', '\u096f', ""), // Devanagari digits
/* Malai */ new Unicode2Ascii('\u0d66', '\u0d6f', ""), // Malayalam digits
/* Thai */ new Unicode2Ascii('\u0e50', '\u0e59', ""), // Thai digits
/* Bengali */ new Unicode2Ascii('\u09e6', '\u09ef', ""), // Bengali digits
/* Gurmukhi */ new Unicode2Ascii('\u0a66', '\u0a6f', ""), // Gurmukhi digits
/* Tibetan */ new Unicode2Ascii('\u0f20', '\u0f29', ""), // Tibetan digits
/* Burmese */ new Unicode2Ascii('\u1040', '\u1049', ""), // Burmese digits
/* Khmer */ new Unicode2Ascii('\u17e0', '\u17e9', ""), // Khmer digits
/* Tamil */ new Unicode2Ascii('\u0be6', '\u0bef', ""), // Tamil digits
/* Amharic */ new Unicode2Ascii('\u1369', '\u1372', "1234567890"), // Amharic digits [1-9][0]
/* Korean */ new Unicode2Ascii('\u1100', '\u1175', "B?CD?F?G?H?OV?WXJKA??????????????????????????????????????????????????????????????????????????????LMNPQER?S???YZ???T?UI"), // Korean
/* Burmese */ new Unicode2Ascii('\u1000', '\u101f', "BCDFGA?J????KLMYNPQESTUWX?HIZORV"), // Burmese
/* Khmer */ new Unicode2Ascii('\u1780', '\u17a2', "BCDFGE?HJAK?LMN??PQRSTV?W?IXO??UY?Z"), // Khmer
/* Sinhalese*/ new Unicode2Ascii('\u0d85', '\u0dc5', "A???E?U??????????????B?C??D??F????G?H??JK?L?M?NP?Q?RSTV?W??X?IYZO"), // Sinhalese
/* Thaana */ new Unicode2Ascii('\u0780', '\u07b1', "BCDFGHJKLMNPQRSTVWXYA?OE????U???????I????????????Z"), // Thaana
/* Chinese */ new Unicode2Ascii('\u3105', '\u3129', "BDFCGHJKLMNRSTUEPQWYZ????I?O??A????XV"), // Chinese
/* Tifinagh */ new Unicode2Ascii('\u2d31', '\u2d63', "B?C???D????FG??H??JKL?M?A???NPI???EOQR??STVW??X?YUZ"), // Tifinagh
/* Tamil */ new Unicode2Ascii('\u0b85', '\u0bb5', "SE?TV????WXY?ZU?B???AC???OD????F???GIH???JKLRMQPN"), // Tamil
/* Amharic */ new Unicode2Ascii('\u1200', '\u1354', "H???????L??????????????????A??E?T?????V?????S???????????????????Q???R???????????????????????????B???C???????????W?????X????????????????????????????M????????????I???????????K???????????????????????????U???????O???????????Z???????????J???????D???????????????????????G???????????????????????????????????????????????Y?????????????????F?????N???P"), // Amharic
/* Telugu */ new Unicode2Ascii('\u0c15', '\u0c35', "BOC?DFIG?AHJK?LMNPQR?SETUV?W?XY?Z"), // Telugu
/* Odia */ new Unicode2Ascii('\u0b15', '\u0b39', "BCDF?G?HA?JOKLEMNPQR?SIT?VWX??Y????ZU"), // Odia
/* Kannada */ new Unicode2Ascii('\u0c85', '\u0cb5', "I?U?O????E???A??BCD?FGHJ???KL?MN?PQR?STVW??X?Y??Z"), // Kannada
/* Gujarati */ new Unicode2Ascii('\u0a87', '\u0ab7', "E?U???O?I?????H?B??J?CXRKNDY?L?F?W?MTGZS?P??A?Q?V"), // Gujarati
// Lowercase variants:
/* Greek */ new Unicode2Ascii('\u03ad', '\u03c9', "EU??ABGDFZHQIKLMNCOJP?STYVXRW"),
/* Georgian */ new Unicode2Ascii('\u10d0', '\u10ef', "AB?CE?D?UF?GHOJ?KLMINPQRSTVW?XYZ"),
/* Armenian */ new Unicode2Ascii('\u0562', '\u0586', "BCDE??FGHI?J?KLM?N?U?PQ?R??STVWXYZ?OA")
};
// @formatter:on
@Nonnull
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;
int rely;
final int codexlen = result.length() - 1; // length ex dot
int prelen = result.indexOf('.'); // dot position
if ((prelen == 1) && (codexlen == 5)) {
prelen++;
result = result.substring(0, 1) + result.charAt(2) + '.' + result.substring(3);
}
final int postlen = codexlen - prelen;
final int divx;
int divy;
divy = DATA_MODEL.getSmartDiv(m);
if (divy == 1) {
divx = Common.X_SIDE[prelen];
divy = Common.Y_SIDE[prelen];
} else {
divx = Common.NC[prelen] / divy;
}
if ((prelen == 4) && (divx == 961) && (divy == 961)) {
result = result.substring(0, 1) + result.charAt(2) + result.charAt(1) + result.substring(3);
}
int v = decodeBase31(result);
if ((divx != divy) && (prelen > 2)) // D==6
{
final Point d = decodeSixWide(v, divx, divy);
relx = d.getLonMicroDeg();
rely = d.getLatMicroDeg();
} else {
relx = v / divy;
rely = divy - 1 - (v % divy);
}
final int ygridsize = (((maxy - miny) + divy) - 1) / divy;
final int xgridsize = (((maxx - minx) + divx) - 1) / divx;
rely = miny + (rely * ygridsize);
relx = minx + (relx * xgridsize);
final int yp = Common.Y_SIDE[postlen];
final int dividery = ((ygridsize + yp) - 1) / yp;
final int xp = Common.X_SIDE[postlen];
final int dividerx = ((xgridsize + xp) - 1) / xp;
String rest = result.substring(prelen + 1);
// decode relative (postfix vs rely, relx)
final int difx;
int dify;
if (postlen == 3) {
final Point d = decodeTriple(rest);
difx = d.getLonMicroDeg();
dify = d.getLatMicroDeg();
} else {
if (postlen == 4) {
rest = String.valueOf(rest.charAt(0)) + rest.charAt(2) + rest.charAt(1) + rest.charAt(3);
}
v = decodeBase31(rest);
difx = v / yp;
dify = v % yp;
}
dify = yp - 1 - dify;
final int cornery = rely + (dify * dividery);
final int cornerx = relx + (difx * dividerx);
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;
final int decodeMaxy = ((rely + ygridsize) < maxy) ? (rely + ygridsize) : maxy;
return decodeExtension(cornery, cornerx, dividerx << 2, dividery, extrapostfix,
0, decodeMaxy, decodeMaxx); // grid
}
@Nonnull
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) {
result = result.substring(0, 3) + result.substring(4);
} else {
result = result.substring(0, 2) + result.substring(3);
}
final int a = Common.countCityCoordinatesForCountry(codexm, firstrec, firstrec);
final int p = 31 / a;
final int r = 31 % a;
int v = 0;
int nrX;
boolean swapletters = false;
if ((codexm != 21) && (a <= 31)) {
final int offset = DECODE_CHARS[(int) result.charAt(0)];
if (offset < (r * (p + 1))) {
nrX = offset / (p + 1);
} else {
swapletters = (p == 1) && (codexm == 22);
nrX = r + ((offset - (r * (p + 1))) / p);
}
} else if ((codexm != 21) && (a < 62)) {
nrX = DECODE_CHARS[(int) result.charAt(0)];
if (nrX < (62 - a)) {
swapletters = codexm == 22;
} else {
nrX = ((nrX + nrX) - 62) + a;
}
} else {
// codex==21 || A>=62
final int basePower = (codexm == 21) ? (961 * 961) : (961 * 961 * 31);
int basePowerA = basePower / a;
if (a == 62) {
basePowerA++;
} else {
basePowerA = 961 * (basePowerA / 961);
}
// decode and determine x
v = decodeBase31(result);
nrX = v / basePowerA;
v %= basePowerA;
}
if (swapletters && !Data.isSpecialShape(firstrec + nrX)) {
result = result.substring(0, 2) + result.charAt(3) + result.charAt(2) + result.charAt(4);
}
if ((codexm != 21) && (a <= 31)) {
v = decodeBase31(result);
if (nrX > 0) {
v -= ((nrX * p) + ((nrX < r) ? nrX : r)) * 961 * 961;
}
} else if ((codexm != 21) && (a < 62)) {
v = decodeBase31(result.substring(1));
if ((nrX >= (62 - a)) && (v >= (16 * 961 * 31))) {
v -= 16 * 961 * 31;
nrX++;
}
}
if (nrX > a) { // past end!
return new MapcodeZone();
}
final int territoryRecord = firstrec + nrX;
int side = DATA_MODEL.getSmartDiv(territoryRecord);
int xSIDE = side;
final Boundary boundary = createBoundaryForTerritoryRecord(territoryRecord);
final int maxx = boundary.getLonMicroDegMax();
final int maxy = boundary.getLatMicroDegMax();
final int minx = boundary.getLonMicroDegMin();
final int miny = boundary.getLatMicroDegMin();
final int dx;
final int dy;
if (Data.isSpecialShape(territoryRecord)) {
xSIDE *= side;
side = 1 + ((maxy - miny) / 90);
xSIDE = xSIDE / side;
final Point d = decodeSixWide(v, xSIDE, side);
dx = d.getLonMicroDeg();
dy = side - 1 - d.getLatMicroDeg();
} else {
dy = v % side;
dx = v / side;
}
if (dx >= xSIDE) // else out-of-range!
{
LOG.error("decodeGrid: Failed, decodeNameless({}): dx {} > xSIDE {}", str, dx, xSIDE);
return new MapcodeZone(); // return undefined (out of range!)
}
final int dividerx4 = Common.xDivider(miny, maxy); // 4 times too large!
final int dividery = 90;
final int cornerx = minx + ((dx * dividerx4) / 4);
final int cornery = maxy - (dy * dividery);
return decodeExtension(cornery, cornerx, dividerx4, -dividery, extrapostfix,
((dx * dividerx4) % 4), miny, maxx); // nameless
}
@Nonnull
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);
int value = decodeBase31(input); // decode top (before dot)
value *= 961 * 31;
final Point triple = decodeTriple(input.substring(input.length() - 3));
// decode bottom 3 chars
int i;
i = m;
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 new MapcodeZone(); // return undefined
}
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);
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) {
final int goodRounder = (codexm >= 23) ? (961 * 961 * 31) : (961 * 961);
product = ((((storageStart + product + goodRounder) - 1) / goodRounder) * goodRounder) - storageStart;
}
if ((value >= storageStart) && (value < (storageStart + product))) {
// code belongs here?
final int dividerx = (((maxx - minx) + w) - 1) / w;
final int dividery = (((maxy - miny) + h) - 1) / h;
value -= storageStart;
value = value / (961 * 31);
int vx = value / (h / 176);
vx = (vx * 168) + triple.getLonMicroDeg();
final int vy = ((value % (h / 176)) * 176) + triple.getLatMicroDeg();
final int cornery = maxy - (vy * dividery);
final int cornerx = minx + (vx * dividerx);
if ((cornerx < minx) || (cornerx >= maxx) || (cornery < miny) || (cornery > maxy)) {
LOG.error("decodeGrid: Failed, decodeAutoHeader({}): corner {}, {} out of bounds", input, cornery, cornerx);
return new MapcodeZone(); // corner out of bounds
}
return decodeExtension(cornery, cornerx, dividerx << 2, -dividery, extrapostfix,
0, miny, maxx); // autoheader
}
storageStart += product;
i++;
}
}
@Nonnull
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);
boolean voweled = false;
final int lastpos = str.length() - 1;
int dotpos = str.indexOf('.');
if ((dotpos < 2) || (lastpos < (dotpos + 2))) {
return ""; // Error: no dot, or less than 2 letters before dot, or
}
// less than 2 letters after dot
if (str.charAt(0) == 'A') { // v1.50
int v1 = DECODE_CHARS[(int) str.charAt(lastpos)];
if (v1 < 0) {
v1 = 31;
}
int v2 = DECODE_CHARS[(int) str.charAt(lastpos - 1)];
if (v2 < 0) {
v2 = 31;
}
final String s = String.valueOf(1000 + v1 + (32 * v2));
str = s.charAt(1) + str.substring(1, lastpos - 1) + s.charAt(2) + s.charAt(3);
voweled = true;
} else if (str.charAt(0) == 'U') { // v.1.50 debug decoding of U+alldigitmapcode
voweled = true;
str = str.substring(1);
dotpos--;
} else {
int v = str.charAt(lastpos - 1);
if (v == 'A') {
v = 0;
} else if (v == 'E') {
v = 34;
} else if (v == 'U') {
v = 68;
} else {
v = -1;
}
if (v >= 0) {
final char e = str.charAt(lastpos);
if (e == 'A') {
v += 31;
} else if (e == 'E') {
v += 32;
} else if (e == 'U') {
v += 33;
} else {
final int ve = DECODE_CHARS[(int) str.charAt(lastpos)];
if (ve < 0) {
return "";
}
v += ve;
}
if (v >= 100) {
return "";
}
voweled = true;
str = str.substring(0, lastpos - 1) + Data.ENCODE_CHARS[v / 10]
+ Data.ENCODE_CHARS[v % 10];
}
}
if ((dotpos < 2) || (dotpos > 5)) {
return "";
}
for (int v = 0; v <= lastpos; v++) {
if (v != dotpos) {
final int i = (int) str.charAt(v);
if (DECODE_CHARS[i] < 0) {
return ""; // bad char!
} else if (voweled && (DECODE_CHARS[(int) str.charAt(v)] > 9)) {
return ""; // no-nodigit!
}
}
}
return str;
}
/**
* This method decodes a Unicode string to ASCII. Package private for access by other modules.
*
* @param mapcode Unicode string.
* @return ASCII string.
*/
@Nonnull
static String decodeUTF16(@Nonnull final String mapcode) {
String result;
final StringBuilder asciiBuf = new StringBuilder();
for (final char ch : mapcode.toCharArray()) {
if (ch == '.') {
asciiBuf.append(ch);
} else if ((ch >= 1) && (ch <= 'z')) {
// normal ascii
asciiBuf.append(ch);
} else {
boolean found = false;
for (final Unicode2Ascii unicode2Ascii : UNICODE2ASCII) {
if ((ch >= unicode2Ascii.min) && (ch <= unicode2Ascii.max)) {
final int pos = ((int) ch) - (int) unicode2Ascii.min;
asciiBuf.append(unicode2Ascii.convert.charAt(pos));
found = true;
break;
}
}
if (!found) {
asciiBuf.append('?');
break;
}
}
}
result = asciiBuf.toString();
// Repack if this was a Greek 'alpha' code. This will have been converted to a regular 'A' after one iteration.
if (mapcode.startsWith(String.valueOf(GREEK_CAPITAL_ALPHA))) {
final String unpacked = aeuUnpack(result);
if (unpacked.isEmpty()) {
throw new AssertionError("decodeUTF16: cannot decode " + mapcode);
}
result = Encoder.aeuPack(unpacked, false);
}
if (isAbjadScript(mapcode)) {
return convertFromAbjad(result);
} else {
return result;
}
}
@Nonnull
static String encodeUTF16(
@Nonnull final String mapcodeInput,
final int alphabetCode) throws IllegalArgumentException {
final String mapcode;
if ((alphabetCode == Alphabet.GREEK.getNumber()) ||
(alphabetCode == Alphabet.HEBREW.getNumber()) ||
(alphabetCode == Alphabet.KOREAN.getNumber()) ||
(alphabetCode == Alphabet.ARABIC.getNumber())) {
mapcode = convertToAbjad(mapcodeInput);
} else {
mapcode = mapcodeInput;
}
final String mapcodeToEncode;
if ((alphabetCode == Alphabet.GREEK.getNumber()) && ((mapcode.indexOf('E') != -1) || (mapcode.indexOf('U') != -1))) {
final String unpacked = aeuUnpack(mapcode);
if (unpacked.isEmpty()) {
throw new IllegalArgumentException("encodeToAlphabetCode: cannot encode '" + mapcode +
"' to alphabet " + alphabetCode);
}
mapcodeToEncode = Encoder.aeuPack(unpacked, true);
} else {
mapcodeToEncode = mapcode;
}
final StringBuilder sb = new StringBuilder();
for (char ch : mapcodeToEncode.toCharArray()) {
ch = Character.toUpperCase(ch);
if (ch > 'Z') {
// Not in any valid range?
sb.append('?');
} else if (ch < 'A') {
// Valid but not a letter (e.g. a dot, a space...). Leave untranslated.
sb.append(ch);
} else {
sb.append(ASCII2LANGUAGE[alphabetCode][(int) ch - (int) 'A']);
}
}
return sb.toString();
}
@Nonnull
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) {
return Point.fromMicroDeg(((c1 / 6) * 34) + (x % 34), ((c1 % 6) * 28) + (x / 34));
}
return Point.fromMicroDeg((x % 40) + 136, (x / 40) + (24 * (c1 - 24)));
}
@Nonnull
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) {
col = maxcol;
d = width - (maxcol * 6);
} else {
d = 6;
}
final int w = v - (col * height * 6);
return Point.fromMicroDeg(height - 1 - (w / d), (col * 6) + (w % d));
}
// / lowest level encode/decode routines
// decode up to dot or EOS;
// returns negative in case of error
private static int decodeBase31(@Nonnull final String code) {
int value = 0;
for (final char c : code.toCharArray()) {
if (c == '.') {
return value;
}
if (DECODE_CHARS[c] < 0) {
return -1;
}
value = (value * 31) + DECODE_CHARS[c];
}
return value;
}
@Nonnull
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;
int lon32 = 0;
int lat32 = 0;
boolean odd = false;
int idx = 0;
// decode up to 8 characters
final int len = (extrapostfix.length() > 8) ? 8 : extrapostfix.length();
while (idx < len) {
int c1 = (int) extrapostfix.charAt(idx);
idx++;
c1 = DECODE_CHARS[c1];
if ((c1 < 0) || (c1 == 30)) {
LOG.error("decodeGrid; Failed, decodeExtension({}): illegal c1 {}", extrapostfix, c1);
return new MapcodeZone();
}
final int y1 = c1 / 5;
final int x1 = c1 % 5;
final int y2;
final int x2;
if (idx < len) {
int c2 = (int) extrapostfix.charAt(idx);
idx++;
c2 = DECODE_CHARS[c2];
if ((c2 < 0) || (c2 == 30)) {
LOG.error("decodeGrid: Failed, decodeExtension({}): illegal c2 {}", extrapostfix, c2);
return new MapcodeZone();
}
y2 = c2 / 6;
x2 = c2 % 6;
} else {
odd = true;
y2 = 0;
x2 = 0;
}
processor *= 30;
lon32 = (lon32 * 30) + (x1 * 6) + x2;
lat32 = (lat32 * 30) + (y1 * 5) + y2;
}
while (processor < Point.MAX_PRECISION_FACTOR) {
dividerx4 *= 30;
dividery *= 30;
processor *= 30;
}
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
mapcodeZone.setFromFractions(lat1, lon4, 5 * dividery, 6 * dividerx4);
} else { // not odd
mapcodeZone.setFromFractions(lat1, lon4, dividery, dividerx4);
} // not odd
// FORCE_RECODE - restrict the coordinate range to the extremes that were provided
if (mapcodeZone.getLonFractionMax() > (maxLonMicroDeg * Point.LON_MICRODEG_TO_FRACTIONS_FACTOR)) {
mapcodeZone.setLonFractionMax(maxLonMicroDeg * Point.LON_MICRODEG_TO_FRACTIONS_FACTOR);
}
if (dividery >= 0) {
if (mapcodeZone.getLatFractionMax() > (extremeLatMicroDeg * Point.LAT_MICRODEG_TO_FRACTIONS_FACTOR)) {
mapcodeZone.setLatFractionMax(extremeLatMicroDeg * Point.LAT_MICRODEG_TO_FRACTIONS_FACTOR);
}
} else {
if (mapcodeZone.getLatFractionMin() < (extremeLatMicroDeg * Point.LAT_MICRODEG_TO_FRACTIONS_FACTOR)) {
mapcodeZone.setLatFractionMin(extremeLatMicroDeg * Point.LAT_MICRODEG_TO_FRACTIONS_FACTOR);
}
}
return mapcodeZone;
}
private static boolean isAbjadScript(@Nonnull final String argStr) {
for (final char ch : argStr.toCharArray()) {
final int c = (int) ch;
if ((c >= 0x0628) && (c <= 0x0649)) {
return true; // Arabic
}
if ((c >= 0x05d0) && (c <= 0x05ea)) {
return true; // Hebrew
}
if ((c >= 0x388) && (c <= 0x3C9)) {
return true; // Greek uppercase and lowercase
}
if (((c >= 0x1100) && (c <= 0x1174)) || ((c >= 0xad6c) && (c <= 0xd314))) {
return true; // Korean
}
}
return false;
}
@Nonnull
private static String convertFromAbjad(@Nonnull final String mapcode) {
// split into prefix, s, postfix
int p = mapcode.lastIndexOf(' ');
if (p < 0) {
p = 0;
} else {
p++;
}
final String prefix = mapcode.substring(0, p);
final String remainder = mapcode.substring(p);
final String postfix;
final int h = remainder.indexOf('-');
final String s;
if (h > 0) {
postfix = remainder.substring(h);
s = aeuUnpack(remainder.substring(0, h));
} else {
postfix = "";
s = aeuUnpack(remainder);
}
final int len = s.length();
final int dot = s.indexOf('.');
if ((dot < 2) || (dot > 5)) {
return mapcode;
}
final int form = (10 * dot) + (len - dot - 1);
String newstr = "";
if (form == 23) {
final int c = (DECODE_CHARS[(int) s.charAt(3)] * 8) + (DECODE_CHARS[(int) s.charAt(4)] - 18);
if ((c >= 0) && (c < 31)) {
newstr = s.substring(0, 2) + '.' + Data.ENCODE_CHARS[c] + s.charAt(5);
}
} else if (form == 24) {
final int c = (DECODE_CHARS[(int) s.charAt(3)] * 8) + (DECODE_CHARS[(int) s.charAt(4)] - 18);
if ((c >= 32) && (c < 63)) {
newstr = s.substring(0, 2) + Data.ENCODE_CHARS[c - 32] + '.' + s.charAt(5) + s.charAt(6);
} else if ((c >= 0) && (c < 31)) {
newstr = s.substring(0, 2) + '.' + Data.ENCODE_CHARS[c % 31] + s.charAt(5) + s.charAt(6);
}
} else if (form == 34) {
final int c = (DECODE_CHARS[(int) s.charAt(2)] * 10) + (DECODE_CHARS[(int) s.charAt(5)] - 7);
if ((c >= 0) && (c < 31)) {
newstr = s.substring(0, 2) + '.' + Data.ENCODE_CHARS[c] + s.charAt(4) + s.charAt(6) + s.charAt(7);
} else if ((c >= 31) && (c < 62)) {
newstr = s.substring(0, 2) + Data.ENCODE_CHARS[c - 31] + '.' + s.charAt(4) + s.charAt(6) + s.charAt(7);
} else if ((c >= 62) && (c < 93)) {
newstr = s.substring(0, 2) + Data.ENCODE_CHARS[c - 62] + s.charAt(4) + '.' + s.charAt(6) + s.charAt(7);
}
} else if (form == 35) {
final int c = ((DECODE_CHARS[(int) s.charAt(2)] * 8) + (DECODE_CHARS[(int) s.charAt(6)] - 18));
if ((c >= 32) && (c < 63)) {
newstr = s.substring(0, 2) + Data.ENCODE_CHARS[c - 32] + s.charAt(4) + '.' + s.charAt(5) + s.charAt(7) +
s.charAt(8);
} else if ((c >= 0) && (c < 31)) {
newstr = s.substring(0, 2) + Data.ENCODE_CHARS[c] + '.' + s.charAt(4) + s.charAt(5) + s.charAt(7) +
s.charAt(8);
}
} else if (form == 45) {
final int c = (DECODE_CHARS[(int) s.charAt(2)] * 100) + (DECODE_CHARS[(int) s.charAt(5)] * 10) +
(DECODE_CHARS[(int) s.charAt(8)] - 39);
if ((c >= 0) && (c < 961)) {
newstr = s.substring(0, 2) + Data.ENCODE_CHARS[c / 31] + s.charAt(3) + '.' + s.charAt(6) + s.charAt(7) +
s.charAt(9) + Data.ENCODE_CHARS[c % 31];
}
} else if (form == 55) {
final int c = (DECODE_CHARS[(int) s.charAt(2)] * 100) + (DECODE_CHARS[(int) s.charAt(6)] * 10) +
(DECODE_CHARS[(int) s.charAt(9)] - 39);
if ((c >= 0) && (c < 961)) {
newstr = s.substring(0, 2) + Data.ENCODE_CHARS[c / 31] + s.charAt(3) + s.charAt(4) + '.' + s.charAt(7) +
s.charAt(8) + s.charAt(10) + Data.ENCODE_CHARS[c % 31];
}
}
if (newstr.isEmpty()) {
return mapcode;
}
return prefix + Encoder.aeuPack(newstr, false) + postfix;
}
@SuppressWarnings("NumericCastThatLosesPrecision")
@Nonnull
private static String convertToAbjad(@Nonnull final String mapcode) {
String str;
final String rest;
final int h = mapcode.indexOf('-');
if (h > 0) {
rest = mapcode.substring(h);
str = aeuUnpack(mapcode.substring(0, h));
} else {
rest = "";
str = aeuUnpack(mapcode);
}
final int len = str.length();
final int dot = str.indexOf('.');
if ((dot < 2) || (dot > 5)) {
return mapcode;
}
final int form = (10 * dot) + (len - dot - 1);
// see if >2 non-digits in a row
int inarow = 0;
for (final char ch : str.toCharArray()) {
if (ch != '.') {
inarow++;
if ((ch >= '0') && (ch <= '9')) {
inarow = 0;
} else if (inarow > 2) {
break;
}
}
}
if ((inarow < 3) && ((form == 22) || (form == 32) || (form == 33) || (form == 42) || (form == 43) || (form == 44) || (form == 54))) {
// no need to do anything
return mapcode;
}
// determine the code of the second non-digit character (before or after the dot)
int c = DECODE_CHARS[(int) str.charAt(2)];
if (c < 0) {
c = DECODE_CHARS[(int) str.charAt(3)];
}
if (c < 0) {
return mapcode; // bad character
}
// create 2 or 3 new digits
final char c1;
final char c2;
final char c3;
if (form >= 44) {
c = (c * 31) + DECODE_CHARS[(int) str.charAt(len - 1)] + 39;
if ((c < 39) || (c > 999)) {
return mapcode; // out of range (last character must be bad)
}
c1 = Data.ENCODE_CHARS[c / 100];
c2 = Data.ENCODE_CHARS[((c % 100) / 10)];
c3 = Data.ENCODE_CHARS[c % 10];
} else if (len == 7) {
if (form == 24) {
c += 7;
} else if (form == 33) {
c += 38;
} else if (form == 42) {
c += 69;
}
c1 = Data.ENCODE_CHARS[c / 10];
c2 = Data.ENCODE_CHARS[c % 10];
c3 = '?';
} else {
c1 = Data.ENCODE_CHARS[2 + (c / 8)];
c2 = Data.ENCODE_CHARS[2 + (c % 8)];
c3 = '?';
}
// re-order the characters
if (form == 22) {
str = str.substring(0, 2) + '.' + c1 + c2 + str.charAt(4);
} else if (form == 23) {
str = str.substring(0, 2) + '.' + c1 + c2 + str.charAt(4) + str.charAt(5);
} else if (form == 32) {
str = str.substring(0, 2) + '.' + (char) ((int) c1 + 4) + c2 + str.charAt(4) + str.charAt(5);
} else if (form == 24) {
str = str.substring(0, 2) + c1 + '.' + str.charAt(4) + c2 + str.charAt(5) + str.charAt(6);
} else if (form == 33) {
str = str.substring(0, 2) + c1 + '.' + str.charAt(4) + c2 + str.charAt(5) + str.charAt(6);
} else if (form == 42) {
str = str.substring(0, 2) + c1 + '.' + str.charAt(3) + c2 + str.charAt(5) + str.charAt(6);
} else if (form == 43) {
str = str.substring(0, 2) + (char) ((int) c1 + 4) + '.' + str.charAt(3) + str.charAt(5) + c2 + str.charAt(6) + str.charAt(7);
} else if (form == 34) {
str = str.substring(0, 2) + c1 + '.' + str.charAt(4) + str.charAt(5) + c2 + str.charAt(6) + str.charAt(7);
} else if (form == 44) {
str = str.substring(0, 2) + c1 + str.charAt(3) + '.' + c2 + str.charAt(5) + str.charAt(6) + c3 + str.charAt(7);
} else if (form == 54) {
str = str.substring(0, 2) + c1 + str.charAt(3) + str.charAt(4) + '.' + c2 + str.charAt(6) + str.charAt(7) + c3 + str.charAt(8);
} else {
// not a valid mapcode form
return mapcode;
}
return Encoder.aeuPack(str + rest, false);
}
}