Skip to content

Commit a0c5dab

Browse files
2.1.1 Enforce and test that encode(decode(m)) delivers mapcode m back
1 parent 1a496e4 commit a0c5dab

File tree

2 files changed

+245
-40
lines changed

2 files changed

+245
-40
lines changed

src/main/java/com/mapcode/Decoder.java

Lines changed: 200 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,97 @@
2020
import org.slf4j.LoggerFactory;
2121

2222
import javax.annotation.Nonnull;
23+
import javax.annotation.Nullable;
24+
25+
class DecodeLimits {
26+
27+
// construct empty
28+
public void DecodeLimits() {
29+
defined = false;
30+
}
31+
32+
// generate upper and lower limits based on x and y, and delta's
33+
public void setFromDelta(final double y, final double x, final double yDelta, final double xDelta) {
34+
if (xDelta < 0) {
35+
maxx = x;
36+
minx = x + xDelta;
37+
}
38+
else {
39+
minx = x;
40+
maxx = x + xDelta;
41+
}
42+
if (yDelta < 0) {
43+
maxy = y;
44+
miny = y + yDelta;
45+
}
46+
else {
47+
miny = y;
48+
maxy = y + yDelta;
49+
}
50+
defined = true;
51+
}
52+
53+
// get minimum y (inclusive)
54+
public double getMinY() {
55+
assert defined;
56+
return miny;
57+
}
58+
59+
// get maximum y (exclusive)
60+
public double getMaxY() {
61+
assert defined;
62+
return maxy;
63+
}
64+
65+
// get minimum x (inclusive)
66+
public double getMinX() {
67+
assert defined;
68+
return minx;
69+
}
70+
71+
// get maximum x (exclusive)
72+
public double getMaxX() {
73+
assert defined;
74+
return maxx;
75+
}
76+
77+
// cut X to specified maximum, if necessary. Returns false if cut results in an empty range
78+
public boolean cutMaxX(int thatMaxX) {
79+
if (maxx >= thatMaxX) { maxx = thatMaxX; }
80+
return (maxx > minx);
81+
}
82+
83+
// cut Y to specified maximum, if necessary. Returns false if cut results in an empty range
84+
public boolean cutMaxY(int thatMaxY) {
85+
if (maxy >= thatMaxY) { maxy = thatMaxY; }
86+
return (maxy > miny);
87+
}
88+
89+
// cut Y to specified minimum, if necessary. Returns false if cut results in an empty range
90+
public boolean cutMinY(int thatMinY) {
91+
if (miny <= thatMinY) { miny = thatMinY; }
92+
return (maxy >= miny);
93+
}
94+
95+
// cut X to specified minimum, if necessary. Returns false if cut results in an empty range
96+
public boolean cutMinX(int thatMinX) {
97+
if (minx <= thatMinX) { minx = thatMinX; }
98+
return (maxx >= minx);
99+
}
100+
101+
@Nonnull
102+
@Override
103+
public String toString() {
104+
return defined ? ("(" + miny + ", " + minx + ")-(" + maxy + ", " + maxx + ")") : "undefined";
105+
}
106+
107+
// private parts
108+
private boolean defined;
109+
private double miny;
110+
private double maxy;
111+
private double minx;
112+
private double maxx;
113+
}
23114

24115
class Decoder {
25116
private static final Logger LOG = LoggerFactory.getLogger(Decoder.class);
@@ -44,8 +135,6 @@ static Point decode(@Nonnull final String argMapcode,
44135
String mapcode = argMapcode;
45136
Territory territory = argTerritory;
46137

47-
// In case of error, result.isDefined() is false.
48-
Point result = Point.undefined();
49138
String extrapostfix = "";
50139

51140
final int minpos = mapcode.indexOf('-');
@@ -59,7 +148,8 @@ static Point decode(@Nonnull final String argMapcode,
59148

60149
mapcode = aeuUnpack(mapcode).trim();
61150
if (mapcode.isEmpty()) {
62-
return result; // failed to decode!
151+
LOG.error("Failed to aeuUnpack {}", argMapcode);
152+
return Point.undefined(); // failed to aeuUnpack
63153
}
64154

65155
final int incodexlen = mapcode.length() - 1;
@@ -90,6 +180,9 @@ static Point decode(@Nonnull final String argMapcode,
90180
final int incodexhi = mapcode.indexOf('.');
91181
final int incodex = (incodexhi * 10) + (incodexlen - incodexhi);
92182

183+
// In case of error, result.isDefined() is false.
184+
Point result = Point.undefined();
185+
93186
for (int i = from; i <= upto; i++) {
94187
final int codexi = Data.calcCodex(i);
95188
if (Data.recType(i) == 0) {
@@ -104,24 +197,58 @@ static Point decode(@Nonnull final String argMapcode,
104197
} else {
105198
// i = grid without headerletter
106199
if ((codexi == incodex) || ((incodex == 22) && (codexi == 21))) {
200+
DecodeLimits decodeLimits = new DecodeLimits();
107201
result = decodeGrid(mapcode,
108202
Data.getBoundaries(i).getMinX(), Data.getBoundaries(i).getMinY(),
109203
Data.getBoundaries(i).getMaxX(), Data.getBoundaries(i).getMaxY(),
110-
i, extrapostfix);
204+
i, extrapostfix, decodeLimits);
111205

112206
if (Data.isRestricted(i) && result.isDefined()) {
113207
boolean fitssomewhere = false;
114208
int j;
115-
for (j = upto - 1; j >= from; j--) {
209+
for (j = i - 1; j >= from; j--) {
116210
if (!Data.isRestricted(j)) {
117211
final int xdiv8 = Common.xDivider(Data.getBoundaries(j).getMinY(),
118212
Data.getBoundaries(j).getMaxY()) / 4;
119-
if (Data.getBoundaries(j).extendBounds(xdiv8, 60).containsPoint(result)) {
213+
if (Data.getBoundaries(j).containsPoint(result)) {
120214
fitssomewhere = true;
121215
break;
122216
}
123217
}
124218
}
219+
220+
if (!fitssomewhere) { // FORCE_RECODE
221+
for (j = from; j < i; j++) { // try all smaller rectangles j
222+
if (!Data.isRestricted(j)) {
223+
int minx = Data.getBoundaries(j).getMinX();
224+
int maxx = Data.getBoundaries(j).getMaxX();
225+
if ((maxx < 0) && (result.getLonDeg() > 0)) {
226+
minx += 360000000;
227+
maxx += 360000000;
228+
}
229+
final int miny = Data.getBoundaries(j).getMinY();
230+
final int maxy = Data.getBoundaries(j).getMaxY();
231+
232+
// force pt within decodeLimits
233+
Point pt = Point.fromPoint(result);
234+
if (miny <= decodeLimits.getMaxY()) { pt.setMinLatToMicroDeg(miny); }
235+
if (maxy > decodeLimits.getMinY()) { pt.setMaxLatToMicroDeg(maxy); }
236+
if (minx <= decodeLimits.getMaxX()) { pt.setMinLonToMicroDeg(minx); }
237+
if (maxx > decodeLimits.getMinX()) { pt.setMaxLonToMicroDeg(maxx); }
238+
239+
// better?
240+
if ( pt.getLatDeg() > decodeLimits.getMinY() && pt.getLatMicroDeg() < decodeLimits.getMaxY() &&
241+
pt.getLonDeg() > decodeLimits.getMinX() && pt.getLonMicroDeg() < decodeLimits.getMaxX() &&
242+
Data.getBoundaries(j).containsPoint(pt))
243+
{
244+
result = Point.fromPoint(pt);
245+
fitssomewhere = true;
246+
break;
247+
}
248+
}
249+
}
250+
} //FORCE_RECODE
251+
125252
if (!fitssomewhere) {
126253
result.setUndefined();
127254
}
@@ -135,7 +262,7 @@ static Point decode(@Nonnull final String argMapcode,
135262
result = decodeGrid(mapcode.substring(1),
136263
Data.getBoundaries(i).getMinX(), Data.getBoundaries(i).getMinY(),
137264
Data.getBoundaries(i).getMaxX(), Data.getBoundaries(i).getMaxY(),
138-
i, extrapostfix);
265+
i, extrapostfix, null);
139266
break;
140267
}
141268
}
@@ -158,15 +285,27 @@ static Point decode(@Nonnull final String argMapcode,
158285

159286
// LIMIT_TO_OUTRECT : make sure it fits the country
160287
if (ccode != CCODE_EARTH) {
161-
final SubArea mapcoderRect = Data.getBoundaries(upto); // find
162-
// encompassing
163-
// rect
164-
final int xdiv8 = Common.xDivider(mapcoderRect.getMinY(), mapcoderRect.getMaxY()) / 4;
288+
final SubArea mapcoderRect = Data.getBoundaries(upto);
289+
final int miny = mapcoderRect.getMinY();
290+
final int maxy = mapcoderRect.getMaxY();
291+
final int xdiv8 = Common.xDivider(miny, maxy) / 4;
165292
// should be /8 but there's some extra margin
166293
if (!mapcoderRect.extendBounds(xdiv8, 60).containsPoint(result)) {
167294
result.setUndefined(); // decodes outside the official territory
168295
// limit
169296
}
297+
else { // FORCE_RECODE
298+
result.setMinLatToMicroDeg(miny);
299+
result.setMaxLatToMicroDeg(maxy);
300+
int minx = mapcoderRect.getMinX();
301+
int maxx = mapcoderRect.getMaxX();
302+
if ((minx > 0) && (result.getLonDeg() < 0)) {
303+
minx -= 360000000;
304+
maxx -= 360000000;
305+
}
306+
result.setMinLonToMicroDeg(minx);
307+
result.setMaxLonToMicroDeg(maxx);
308+
} // FORCE_RECODE
170309
}
171310
}
172311

@@ -267,7 +406,7 @@ public Unicode2Ascii(final char min, final char max, @Nonnull final String conve
267406

268407
@Nonnull
269408
private static Point decodeGrid(final String str, final int minx, final int miny, final int maxx, final int maxy,
270-
final int m, final String extrapostfix) {
409+
final int m, final String extrapostfix, DecodeLimits decodeLimits) {
271410
// for a well-formed result, and integer variables
272411
String result = str;
273412
int relx;
@@ -342,7 +481,16 @@ private static Point decodeGrid(final String str, final int minx, final int miny
342481
final int cornery = rely + (dify * dividery);
343482
final int cornerx = relx + (difx * dividerx);
344483

345-
return decodeExtension(cornery, cornerx, dividerx << 2, dividery, extrapostfix, 0); // grid
484+
Point pt = Point.fromMicroDeg(cornery,cornerx);
485+
if (!(Data.getBoundaries(m).containsPoint(pt))) {
486+
LOG.info("*** decodeGrid({}) : {} not in {}", str, pt, Data.getBoundaries(m));
487+
return Point.undefined(); // already out of range
488+
}
489+
490+
final int decodeMaxx = ((relx + xgridsize) < maxx) ? (relx + xgridsize) : maxx;
491+
final int decodeMaxy = ((rely + ygridsize) < maxy) ? (rely + ygridsize) : maxy;
492+
return decodeExtension(decodeLimits, cornery, cornerx, dividerx << 2, dividery, extrapostfix,
493+
0, decodeMaxy, decodeMaxx); // grid
346494
}
347495

348496
@Nonnull
@@ -417,6 +565,7 @@ private static Point decodeNameless(final String str, final int firstrec, final
417565
int side = DataAccess.smartDiv(m);
418566
int xSIDE = side;
419567

568+
final int maxx = Data.getBoundaries(m).getMaxX();
420569
final int maxy = Data.getBoundaries(m).getMaxY();
421570
final int minx = Data.getBoundaries(m).getMinX();
422571
final int miny = Data.getBoundaries(m).getMinY();
@@ -439,6 +588,7 @@ private static Point decodeNameless(final String str, final int firstrec, final
439588

440589
if (dx >= xSIDE) // else out-of-range!
441590
{
591+
LOG.error("*** decodeNameless({}) : dx {} > xSIDE {}", str, dx, xSIDE);
442592
return Point.undefined(); // return undefined (out of range!)
443593
}
444594

@@ -447,7 +597,8 @@ private static Point decodeNameless(final String str, final int firstrec, final
447597

448598
final int cornerx = minx + ((dx * dividerx4) / 4);
449599
final int cornery = maxy - (dy * dividery);
450-
return decodeExtension(cornery, cornerx, dividerx4, -dividery, extrapostfix, ((dx * dividerx4) % 4) ); // nameless
600+
return decodeExtension(null, cornery, cornerx, dividerx4, -dividery, extrapostfix,
601+
((dx * dividerx4) % 4), miny, maxx); // nameless
451602
}
452603

453604
@Nonnull
@@ -465,6 +616,7 @@ private static Point decodeAutoHeader(final String input, final int m, final Str
465616
i = m;
466617
while (true) {
467618
if ((Data.recType(i)<2) || (Data.calcCodex(i) != codexm)) {
619+
LOG.error("*** decodeAutoHeader({}) : out of {} records", input, codexm);
468620
return Point.undefined(); // return undefined
469621
}
470622

@@ -503,10 +655,12 @@ private static Point decodeAutoHeader(final String input, final int m, final Str
503655
final int cornerx = minx + (vx * dividerx);
504656

505657
if (cornerx < minx || cornerx >= maxx || cornery < miny || cornery > maxy) {
658+
LOG.error("*** decodeAutoHeader({}) : corner {}, {} out of bounds", input, cornery, cornerx);
506659
return Point.undefined(); // corner out of bounds
507660
}
508-
509-
return decodeExtension(cornery, cornerx, dividerx << 2, -dividery, extrapostfix, 0); // autoheader
661+
662+
return decodeExtension(null, cornery, cornerx, dividerx << 2, -dividery, extrapostfix,
663+
0, miny, maxx); // autoheader
510664
}
511665
storageStart += product;
512666
i++;
@@ -717,7 +871,9 @@ private static int decodeBase31(final String code) {
717871
}
718872

719873
@Nonnull
720-
private static Point decodeExtension(final int y, final int x, final int dividerx4, final int dividery0, final String extrapostfix, final int lon_offset4) {
874+
private static Point decodeExtension(@Nullable DecodeLimits decodeLimits, final int y, final int x, final int dividerx4, final int dividery0,
875+
final String extrapostfix, final int lon_offset4, final int extremeLat32, final int maxLon32) {
876+
if (decodeLimits==null) decodeLimits = new DecodeLimits();
721877
final double dividerx = dividerx4 / 4.0, dividery = (double) dividery0;
722878
double processor = 1;
723879
int lon32 = 0;
@@ -730,6 +886,7 @@ private static Point decodeExtension(final int y, final int x, final int divider
730886
int c1 = (int) extrapostfix.charAt(idx++);
731887
c1 = DECODE_CHARS[c1];
732888
if (c1 < 0 || c1 == 30) {
889+
LOG.error("*** decodeExtension({}) : illegal c1 {}", extrapostfix, c1);
733890
return Point.undefined();
734891
}
735892
final int y1 = c1 / 5;
@@ -740,6 +897,7 @@ private static Point decodeExtension(final int y, final int x, final int divider
740897
int c2 = (int) extrapostfix.charAt(idx++);
741898
c2 = DECODE_CHARS[c2];
742899
if (c2 < 0 || c2 == 30) {
900+
LOG.error("*** decodeExtension({}) : illegal c2 {}", extrapostfix, c2);
743901
return Point.undefined();
744902
}
745903
y2 = c2 / 6;
@@ -758,24 +916,32 @@ private static Point decodeExtension(final int y, final int x, final int divider
758916
double lat = y + ((lat32 * dividery) / processor);
759917
double lon = x + ((lon32 * dividerx) / processor) + ( lon_offset4 / 4.0 );
760918

761-
/* FORCE_RECODE : TO DO!
762-
var range = {minlon:lon, maxlon:lon, minlat:lat, maxlat:lat};
763-
if (odd) {
764-
range.maxlon += (dividerx / (processor / 6));
765-
range.maxlat += (dividery / (processor / 5));
766-
} else {
767-
range.maxlon += (dividerx / processor);
768-
range.maxlat += (dividery / processor);
769-
} // FORCE_RECODE */
770-
771-
if (odd) {
772-
lon += (dividerx / (2 * (processor / 6)));
773-
lat += (dividery / (2 * (processor / 5)));
774-
} else {
775-
lon += (dividerx / (2 * processor));
776-
lat += (dividery / (2 * processor));
919+
// determine the range of coordinates that are encode to this mapcode
920+
if (odd) { // odd
921+
decodeLimits.setFromDelta(lat, lon, (dividery / (processor / 5)), (dividerx / (processor / 6)) );
922+
} else { // not odd
923+
decodeLimits.setFromDelta(lat, lon, (dividery / processor), (dividerx / processor) );
777924
} // not odd
778925

779-
return Point.fromDeg( lat / 1000000.0, lon / 1000000.0 );
926+
// FORCE_RECODE - restrict the coordinate range to the extremes that were provided
927+
if (decodeLimits.cutMaxX(maxLon32)==false) {
928+
LOG.error("*** decodeExtension({}) : cutMaxX {}", extrapostfix, maxLon32);
929+
return Point.undefined();
930+
}
931+
if (dividery >= 0 )
932+
{
933+
if (decodeLimits.cutMaxY(extremeLat32)==false) {
934+
LOG.error("*** decodeExtension({}) : cutMaxY {}", extrapostfix, extremeLat32);
935+
return Point.undefined();
936+
}
937+
} else { // dividery < 0
938+
if (decodeLimits.cutMinY(extremeLat32)==false) {
939+
LOG.error("*** decodeExtension({}) : cutMinY {}", extrapostfix, extremeLat32);
940+
return Point.undefined();
941+
}
942+
}
943+
944+
// return a coordinate exactly in the middle of the range
945+
return Point.fromDeg( (decodeLimits.getMinY() + decodeLimits.getMaxY()) / 2000000.0, (decodeLimits.getMinX() + decodeLimits.getMaxX()) / 2000000.0 );
780946
}
781947
}

0 commit comments

Comments
 (0)