aboutsummaryrefslogtreecommitdiffstats
path: root/main/src/cgeo/geocaching/connector/gc/IconDecoder.java
diff options
context:
space:
mode:
authormucek4 <tomaz@gorenc.org>2012-12-13 14:27:12 +0100
committermucek4 <tomaz@gorenc.org>2012-12-13 21:01:43 +0100
commit84c1f2abae51a6271731792f46217d396e67a35a (patch)
tree75b1e403a65be5a9098809389b8f08a563412853 /main/src/cgeo/geocaching/connector/gc/IconDecoder.java
parent5ad007ed7938a9b284278b34a77ec21a0061fd86 (diff)
downloadcgeo-84c1f2abae51a6271731792f46217d396e67a35a.zip
cgeo-84c1f2abae51a6271731792f46217d396e67a35a.tar.gz
cgeo-84c1f2abae51a6271731792f46217d396e67a35a.tar.bz2
Better PNG parsing of live map
Diffstat (limited to 'main/src/cgeo/geocaching/connector/gc/IconDecoder.java')
-rw-r--r--main/src/cgeo/geocaching/connector/gc/IconDecoder.java610
1 files changed, 515 insertions, 95 deletions
diff --git a/main/src/cgeo/geocaching/connector/gc/IconDecoder.java b/main/src/cgeo/geocaching/connector/gc/IconDecoder.java
index 74e78cc..554760c 100644
--- a/main/src/cgeo/geocaching/connector/gc/IconDecoder.java
+++ b/main/src/cgeo/geocaching/connector/gc/IconDecoder.java
@@ -11,106 +11,193 @@ import android.graphics.Bitmap;
*/
public abstract class IconDecoder {
- public static void parseMapPNG(final cgCache cache, Bitmap bitmap, UTFGridPosition xy, int zoomlevel) {
+ public static boolean parseMapPNG(final cgCache cache, Bitmap bitmap, UTFGridPosition xy, int zoomlevel) {
if (zoomlevel >= 14) {
- parseMapPNG14(cache, bitmap, xy);
- } else {
- parseMapPNG13(cache, bitmap, xy);
+ return parseMapPNG14(cache, bitmap, xy);
}
+ if (zoomlevel <= 11) {
+ return parseMapPNG11(cache, bitmap, xy);
+ }
+ return parseMapPNG13(cache, bitmap, xy);
}
- private static final int[] OFFSET_X = new int[] { 0, -1, -1, 0, 1, 1, 1, 0, -1, -2, -2, -2, -2, -1, 0, 1, 2, 2, 2, 2, 2, 1, 0, -1, -2 };
- private static final int[] OFFSET_Y = new int[] { 0, 0, 1, 1, 1, 0, -1, -1, -1, -1, 0, 1, 2, 2, 2, 2, 2, 1, 0, -1, -2, -2, -2, -2, -2 };
+ public static int CT_TRADITIONAL = 0;
+ public static int CT_MULTI = 1;
+ public static int CT_MYSTERY = 2;
+ public static int CT_EVENT = 3;
+ public static int CT_VIRTUAL = 4;
+ public static int CT_FOUND = 5;
+ public static int CT_OWN = 6;
+ public static int CT_MEGAEVENT = 7;
+ public static int CT_CITO = 8;
+ public static int CT_WEBCAM = 9;
+ public static int CT_WHEREIGO = 10;
+ public static int CT_EARTH = 11;
+ public static int CT_LETTERBOX = 12;
/**
- * The icon decoder walks a spiral around the center pixel position of the cache
- * and searches for characteristic colors.
+ * The icon decoder over all 16 pixels of image . It should not be invoked on any part of image that might overlay
+ * with other caches.
+ * Is uses decision tree to determine right type.
*
* @param cache
* @param bitmap
* @param xy
+ * @return true if parsing was successful
*/
- private static void parseMapPNG13(final cgCache cache, Bitmap bitmap, UTFGridPosition xy) {
- final int xCenter = xy.getX() * 4 + 2;
- final int yCenter = xy.getY() * 4 + 2;
+ private static boolean parseMapPNG13(final cgCache cache, Bitmap bitmap, UTFGridPosition xy) {
+ final int topX = xy.getX() * 4;
+ final int topY = xy.getY() * 4;
final int bitmapWidth = bitmap.getWidth();
final int bitmapHeight = bitmap.getHeight();
- int countMulti = 0;
- int countFound = 0;
+ int[] pngType = new int[7];
- for (int i = 0; i < OFFSET_X.length; i++) {
+ if ((topX < 0) || (topY < 0) || (topX + 4 > bitmapWidth) || (topY + 4 > bitmapHeight)) {
+ return false; //out of image position
+ }
- // assert that we are still in the tile
- final int x = xCenter + OFFSET_X[i];
- if (x < 0 || x >= bitmapWidth) {
- continue;
- }
+ for (int x = topX; x < topX + 4; x++) {
+ for (int y = topY; y < topY + 4; y++) {
+ int color = bitmap.getPixel(x, y);
- final int y = yCenter + OFFSET_Y[i];
- if (y < 0 || y >= bitmapHeight) {
- continue;
- }
+ if ((color & 0xFFFFFF) == 0x5f5f5f) {
+ continue; //Border in every icon is the same and therefore no use to us
+ }
+ if ((color >>> 24) != 255) {
+ continue; //transparent pixels (or semi_transparent) are only shadows of border
+ }
- int color = bitmap.getPixel(x, y) & 0x00FFFFFF;
+ int red = (color & 0xFF0000) >> 16;
+ int green = (color & 0xFF00) >> 8;
+ int blue = color & 0xFF;
- // transparent pixels are not interesting
- if (color == 0) {
- continue;
+ int type = getCacheTypeFromPixel13(red, green, blue);
+ pngType[type]++;
}
+ }
- int red = (color & 0xFF0000) >> 16;
- int green = (color & 0xFF00) >> 8;
- int blue = color & 0xFF;
+ int type = -1;
+ int count = 0;
- // these are quite sure, so one pixel is enough for matching
- if (green > 0x80 && green > red && green > blue) {
- cache.setType(CacheType.TRADITIONAL);
- return;
- }
- if (blue > 0x80 && blue > red && blue > green) {
- cache.setType(CacheType.MYSTERY);
- return;
- }
- if (red > 0x90 && blue < 0x10 && green < 0x10) {
- cache.setType(CacheType.EVENT);
- return;
+ for (int x = 0; x < 7; x++)
+ {
+ if (pngType[x] > count) {
+ count = pngType[x];
+ type = x;
}
+ }
- // next two are hard to distinguish, therefore we sample all pixels of the spiral
- if (red > 0xFA && green > 0xD0) {
- countMulti++;
+ if (count > 1) { // 2 pixels need to detect same type and we say good to go
+ switch (type) {
+ case 0:
+ cache.setType(CacheType.TRADITIONAL);
+ return true;
+ case 1:
+ cache.setType(CacheType.MULTI);
+ return true;
+ case 2:
+ cache.setType(CacheType.MYSTERY); //mystery, whereigo, groundspeak HQ and mystery is most common
+ return true;
+ case 3:
+ cache.setType(CacheType.EVENT); //event, cito, mega-event and event is most common
+ return true;
+ case 4:
+ cache.setType(CacheType.EARTH); //It's an image of ghost (webcam, earth, virtual) and earth in most common
+ return true;
+ case 5:
+ cache.setFound(true);
+ return true;
+ case 6:
+ cache.setOwn(true);
+ return true;
}
- if (red < 0xF3 && red > 0xa0 && green > 0x20 && blue < 0x80) {
- countFound++;
+ }
+ return false;
+ }
+
+ /**
+ * The icon decoder over all 16 pixels of image . It should not be invoked on any part of image that might overlay
+ * with other caches.
+ * Is uses decision tree to determine right type.
+ *
+ * @param cache
+ * @param bitmap
+ * @param xy
+ * @return true if parsing was successful
+ */
+ private static boolean parseMapPNG11(final cgCache cache, Bitmap bitmap, UTFGridPosition xy) {
+ final int topX = xy.getX() * 4;
+ final int topY = xy.getY() * 4;
+ final int bitmapWidth = bitmap.getWidth();
+ final int bitmapHeight = bitmap.getHeight();
+
+ int[] pngType = new int[5];
+
+ if ((topX < 0) || (topY < 0) || (topX + 4 > bitmapWidth) || (topY + 4 > bitmapHeight)) {
+ return false; //out of image position
+ }
+
+ for (int x = topX; x < topX + 4; x++) {
+ for (int y = topY; y < topY + 4; y++) {
+ int color = bitmap.getPixel(x, y);
+
+
+ if ((color >>> 24) != 255) {
+ continue; //transparent pixels (or semi_transparent) are only shadows of border
+ }
+
+ int r = (color & 0xFF0000) >> 16;
+ int g = (color & 0xFF00) >> 8;
+ int b = color & 0xFF;
+
+ //Duplicate colors does not add any value
+ if (((r == 52) && (g == 52) && (b == 52)) ||
+ ((r == 69) && (g == 69) && (b == 69)) ||
+ ((r == 90) && (g == 90) && (b == 90)) ||
+ ((r == 233) && (g == 233) && (b == 234)) ||
+ ((r == 255) && (g == 255) && (b == 255))) {
+ continue;
+ }
+
+ int type = getCacheTypeFromPixel11(r, g, b);
+ pngType[type]++;
}
}
- // now check whether we are sure about found/multi
- if (countFound > countMulti && countFound >= 2) {
- cache.setFound(true);
+ int type = -1;
+ int count = 0;
+
+ for (int x = 0; x < 5; x++)
+ {
+ if (pngType[x] > count) {
+ count = pngType[x];
+ type = x;
+ }
}
- if (countMulti > countFound && countMulti >= 5) {
- cache.setType(CacheType.MULTI);
+
+ if (count > 1) { // 2 pixels need to detect same type and we say good to go
+ switch (type) {
+ case 0:
+ cache.setType(CacheType.TRADITIONAL);
+ return true;
+ case 1:
+ cache.setType(CacheType.MULTI);
+ return true;
+ case 2:
+ cache.setType(CacheType.MYSTERY); //mystery, whereigo, groundspeak HQ and mystery is most common
+ return true;
+ case 3:
+ cache.setType(CacheType.EVENT); //event, cito, mega-event and event is most common
+ return true;
+ case 4:
+ cache.setType(CacheType.EARTH); //webcam, earth, virtual and earth in most common
+ return true;
+ }
}
+ return false;
}
- // Pixel colors in tile
- private final static int COLOR_BORDER_GRAY = 0x5F5F5F;
- private final static int COLOR_TRADITIONAL = 0x316013;
- private final static int COLOR_MYSTERY = 0x243C97;
- private final static int COLOR_MULTI = 0xFFDE19;
- private final static int COLOR_FOUND = 0xFBEA5D;
-
- // Offset inside cache icon
- private final static int POSX_TRADI = 7;
- private final static int POSY_TRADI = -12;
- private final static int POSX_MULTI = 5; // for orange 8
- private final static int POSY_MULTI = -9; // for orange 10
- private final static int POSX_MYSTERY = 5;
- private final static int POSY_MYSTERY = -13;
- private final static int POSX_FOUND = 10;
- private final static int POSY_FOUND = -8;
/**
* For level 14 find the borders of the icons and then use a single pixel and color to match.
@@ -119,44 +206,377 @@ public abstract class IconDecoder {
* @param bitmap
* @param xy
*/
- private static void parseMapPNG14(cgCache cache, Bitmap bitmap, UTFGridPosition xy) {
- int x = xy.getX() * 4 + 2;
- int y = xy.getY() * 4 + 2;
+ private static boolean parseMapPNG14(cgCache cache, Bitmap bitmap, UTFGridPosition xy) {
+ final int topX = xy.getX() * 4;
+ final int topY = xy.getY() * 4;
+ final int bitmapWidth = bitmap.getWidth();
+ final int bitmapHeight = bitmap.getHeight();
+
+ int[] pngType = new int[13];
+
+ if ((topX < 0) || (topY < 0) || (topX + 4 > bitmapWidth) || (topY + 4 > bitmapHeight)) {
+ return false; //out of image position
+ }
+
+ for (int x = topX; x < topX + 4; x++) {
+ for (int y = topY; y < topY + 4; y++) {
+ int color = bitmap.getPixel(x, y);
+
+ if ((color & 0xFFFFFF) == 0x5f5f5f) {
+ continue; //Border in every icon is the same and therefore no use to us
+ }
+ if ((color >>> 24) != 255) {
+ continue; //transparent pixels (or semi_transparent) are only shadows of border
+ }
+
+ int r = (color & 0xFF0000) >> 16;
+ int g = (color & 0xFF00) >> 8;
+ int b = color & 0xFF;
+
+ //Duplicate colors does not add any value
+ if (((r == 216) && (g == 216) && (b == 216)) ||
+ ((r == 23) && (g == 23) && (b == 23)) ||
+ ((r == 240) && (g == 240) && (b == 240)) ||
+ ((r == 44) && (g == 44) && (b == 44)) ||
+ ((r == 228) && (g == 228) && (b == 228)) ||
+ ((r == 225) && (g == 225) && (b == 225)) ||
+ ((r == 199) && (g == 199) && (b == 199)) ||
+ ((r == 161) && (g == 161) && (b == 161)) ||
+ ((r == 8) && (g == 8) && (b == 8)) ||
+ ((r == 200) && (g == 200) && (b == 200)) ||
+ ((r == 255) && (g == 255) && (b == 255)) ||
+ ((r == 250) && (g == 250) && (b == 250)) ||
+ ((r == 95) && (g == 95) && (b == 95)) ||
+ ((r == 236) && (g == 236) && (b == 236)) ||
+ ((r == 215) && (g == 215) && (b == 215)) ||
+ ((r == 232) && (g == 232) && (b == 232)) ||
+ ((r == 217) && (g == 217) && (b == 217)) ||
+ ((r == 0) && (g == 0) && (b == 0)) ||
+ ((r == 167) && (g == 167) && (b == 167)) ||
+ ((r == 247) && (g == 247) && (b == 247)) ||
+ ((r == 144) && (g == 144) && (b == 144)) ||
+ ((r == 231) && (g == 231) && (b == 231)) ||
+ ((r == 248) && (g == 248) && (b == 248))) {
+ continue;
+ }
- // search for left border
- int countX = 0;
- while ((bitmap.getPixel(x, y) & 0x00FFFFFF) != COLOR_BORDER_GRAY) {
- if (--x < 0 || ++countX > 20) {
- return;
+ int type = getCacheTypeFromPixel14(r, g, b);
+ pngType[type]++;
}
}
- // search for bottom border
- int countY = 0;
- while ((bitmap.getPixel(x, y) & 0x00FFFFFF) != 0x000000) {
- if (++y >= Tile.TILE_SIZE || ++countY > 20) {
- return;
+
+ int type = -1;
+ int count = 0;
+
+ for (int x = 0; x < 7; x++)
+ {
+ if (pngType[x] > count) {
+ count = pngType[x];
+ type = x;
+ }
+ }
+ /*
+ * public static int CT_MEGAEVENT = 7;
+ * public static int CT_CITO = 8;
+ * public static int CT_WEBCAM = 9;
+ * public static int CT_WHEREIGO = 10;
+ * public static int CT_EARTH = 11;
+ * public static int CT_LETTERBOX = 12;
+ */
+ if (count > 1) { // 2 pixels need to detect same type and we say good to go
+ switch (type) {
+ case 0:
+ cache.setType(CacheType.TRADITIONAL);
+ return true;
+ case 1:
+ cache.setType(CacheType.MULTI);
+ return true;
+ case 2:
+ cache.setType(CacheType.MYSTERY);
+ return true;
+ case 3:
+ cache.setType(CacheType.EVENT);
+ return true;
+ case 4:
+ cache.setType(CacheType.VIRTUAL);
+ return true;
+ case 5:
+ cache.setFound(true);
+ return true;
+ case 6:
+ cache.setOwn(true);
+ return true;
+ case 7:
+ cache.setType(CacheType.MEGA_EVENT);
+ return true;
+ case 8:
+ cache.setType(CacheType.CITO);
+ return true;
+ case 9:
+ cache.setType(CacheType.WEBCAM);
+ return true;
+ case 10:
+ cache.setType(CacheType.WHERIGO);
+ return true;
+ case 11:
+ cache.setType(CacheType.EARTH);
+ return true;
+ case 12:
+ cache.setType(CacheType.LETTERBOX);
+ return true;
+ }
+ }
+ return false;
+
+ }
+
+ /**
+ * This method returns detected type from specific pixel from geocaching.com live map.
+ * It was constructed based on classification tree made by Orange (http://orange.biolab.si/)
+ * Input file was made from every non-transparent pixel of every possible "middle" cache icon from GC map
+ *
+ * @param r
+ * Red component of pixel (from 0 - 255)
+ * @param g
+ * Green component of pixel (from 0 - 255)
+ * @param b
+ * Blue component of pixel (from 0 - 255)
+ * @return Value from 0 to 6 representing detected type or state of the cache.
+ */
+ private static int getCacheTypeFromPixel13(int r, int g, int b) {
+ if (g < 110) {
+ if (r > 87) {
+ return ((g > 73) && (b < 63)) ? CT_FOUND : CT_EVENT;
+ }
+ return CT_MYSTERY;
+ }
+ if (b > 137) {
+ if ((r < 184) && (g > 190)) {
+ return CT_TRADITIONAL;
+ }
+ if ((r < 184) && (g < 191) && (r < 136)) {
+ return CT_MYSTERY;
}
+ return CT_VIRTUAL;
}
+ if (r < 158) {
+ return ((r > 129) && (r < 153)) ? CT_FOUND : CT_TRADITIONAL;
+ }
+ if (b > 33) {
+ if (b > 57) {
+ if (b > 100) {
+ return (r > 229) ? CT_MULTI : CT_EVENT;
+ }
+ return ((r > 254) && (g < 236)) ? CT_MULTI : CT_FOUND;
+ }
+ if ((g > 173) && ((g < 224))) {
+ return ((r < 243) && (r > 223)) ? CT_FOUND : CT_OWN;
+ }
+ return CT_FOUND;
+ }
+ return CT_MULTI;
+ }
- try {
- if ((bitmap.getPixel(x + POSX_TRADI, y + POSY_TRADI) & 0x00FFFFFF) == COLOR_TRADITIONAL) {
- cache.setType(CacheType.TRADITIONAL);
- return;
+ /**
+ * This method returns detected type from specific pixel from geocaching.com live map level 14 or higher.
+ * It was constructed based on classification tree made by Orange (http://orange.biolab.si/)
+ * Input file was made from every non-transparent pixel of every possible "full" cache icon from GC map
+ *
+ * @param r
+ * Red component of pixel (from 0 - 255)
+ * @param g
+ * Green component of pixel (from 0 - 255)
+ * @param b
+ * Blue component of pixel (from 0 - 255)
+ * @return Value from 0 to 6 representing detected type or state of the cache.
+ */
+ private static int getCacheTypeFromPixel14(int r, int g, int b) {
+ if (b < 140) {
+ if (r > 155) {
+ if (g < 159) {
+ if (r < 173) {
+ return (r > 161) ? CT_MEGAEVENT : CT_OWN;
+ }
+ if (r < 206) {
+ if (b > 49) {
+ return (b > 83) ? CT_EVENT : CT_FOUND;
+ }
+ return (b < 31) ? CT_EARTH : CT_FOUND;
+ }
+ return (r < 221) ? CT_FOUND : CT_MULTI;
+ }
+ if (r > 210) {
+ if (g < 188) {
+ return CT_FOUND;
+ }
+ if (r < 246) {
+ return CT_OWN;
+ }
+ if (r < 254) {
+ return CT_FOUND;
+ }
+ if (r < 255) {
+ return CT_EVENT;
+ }
+ if (g < 208) {
+ return CT_EARTH;
+ }
+ if (g > 225) {
+ return CT_EARTH;
+ }
+ return (b < 36) ? CT_MULTI : CT_OWN;
+ }
+ return (b < 66) ? CT_OWN : CT_EARTH;
+ }
+ if (r < 63) {
+ if (b > 26) {
+ if (b < 29) {
+ return CT_WEBCAM;
+ }
+ if (g > 102) {
+ return CT_CITO;
+ }
+ return (r < 26) ? CT_CITO : CT_WEBCAM;
+ }
+ if (g < 38) {
+ return CT_WEBCAM;
+ }
+ return (r < 41) ? CT_EARTH : CT_TRADITIONAL;
+ }
+ if (b < 119) {
+ if (g < 81) {
+ return CT_WEBCAM;
+ }
+ if (b < 90) {
+ return CT_OWN;
+ }
+ return (r < 104) ? CT_WEBCAM : CT_OWN;
+ }
+ if (r < 132) {
+ return (b < 124) ? CT_MULTI : CT_WHEREIGO;
+ }
+ if (g > 164) {
+ return CT_TRADITIONAL;
+ }
+ if (b < 134) {
+ return CT_OWN;
+ }
+ return (b > 137) ? CT_OWN : CT_WHEREIGO;
+ }
+ if (b < 245) {
+ if (r < 180) {
+ if (b < 218) {
+ if (g < 71) {
+ return CT_MYSTERY;
+ }
+ if (r < 96) {
+ return CT_WHEREIGO;
+ }
+ if (b > 165) {
+ return CT_WHEREIGO;
+ }
+ if (r < 153) {
+ return CT_WHEREIGO;
+ }
+ if (r < 160) {
+ return CT_WEBCAM;
+ }
+ return (r < 162) ? CT_WHEREIGO : CT_WEBCAM;
+ }
+ return (r < 158) ? CT_MEGAEVENT : CT_EARTH;
}
- if ((bitmap.getPixel(x + POSX_MYSTERY, y + POSY_MYSTERY) & 0x00FFFFFF) == COLOR_MYSTERY) {
- cache.setType(CacheType.MYSTERY);
- return;
+ if (g > 232) {
+ if (g > 247) {
+ return CT_CITO;
+ }
+ if (r < 237) {
+ return CT_OWN;
+ }
+ if (g < 238) {
+ return CT_OWN;
+ }
+ if (r > 243) {
+ return CT_WEBCAM;
+ }
+ return (g > 238) ? CT_OWN : CT_WEBCAM;
}
- if ((bitmap.getPixel(x + POSX_MULTI, y + POSY_MULTI) & 0x00FFFFFF) == COLOR_MULTI) {
- cache.setType(CacheType.MULTI);
- return;
+ if (r < 228) {
+ if (b > 238) {
+ return CT_MYSTERY;
+ }
+ if (r < 193) {
+ if (r < 184) {
+ return CT_OWN;
+ }
+ if (g < 186) {
+ return CT_WHEREIGO;
+ }
+ return (r > 189) ? CT_WHEREIGO : CT_OWN;
+ }
+ if (g < 223) {
+ if (r > 216) {
+ return CT_OWN;
+ }
+ if (g > 217) {
+ return CT_WHEREIGO;
+ }
+ if (b > 211) {
+ return CT_WEBCAM;
+ }
+ if (b < 196) {
+ return CT_WEBCAM;
+ }
+ if (r > 210) {
+ return CT_OWN;
+ }
+ return (g > 206) ? CT_WHEREIGO : CT_OWN;
+ }
+ if (g < 224) {
+ return CT_OWN;
+ }
+ return (r < 226) ? CT_WHEREIGO : CT_OWN;
}
- if ((bitmap.getPixel(x + POSX_FOUND, y + POSY_FOUND) & 0x00FFFFFF) == COLOR_FOUND) {
- cache.setFound(true);
+ return (b < 216) ? CT_FOUND : CT_OWN;
+ }
+ if (r < 238) {
+ if (r > 141) {
+ return (r > 185) ? CT_LETTERBOX : CT_CITO;
}
- } catch (IllegalArgumentException e) {
- // intentionally left blank
+ return (r < 41) ? CT_EARTH : CT_LETTERBOX;
}
+ return (r < 243) ? CT_WHEREIGO : CT_OWN;
+ }
+ /**
+ * This method returns detected type from specific pixel from geocaching.com live map level 11 or lower.
+ * It was constructed based on classification tree made by Orange (http://orange.biolab.si/)
+ * Input file was made from every non-transparent pixel of every possible "full" cache icon from GC map
+ *
+ * @param r
+ * Red component of pixel (from 0 - 255)
+ * @param g
+ * Green component of pixel (from 0 - 255)
+ * @param b
+ * Blue component of pixel (from 0 - 255)
+ * @return Value from 0 to 4 representing detected type or state of the cache.
+ */
+ private static int getCacheTypeFromPixel11(int r, int g, int b) {
+ if (b < 139) {
+ if (g > 104) {
+ if (r < 173) {
+ return CT_TRADITIONAL;
+ }
+ return (r > 225) ? CT_MULTI : CT_EVENT;
+ }
+ if (b < 25) {
+ return CT_EVENT;
+ }
+ return (r < 87) ? CT_MYSTERY : CT_EVENT;
+ }
+ if (r > 140) {
+ return (r < 197) ? CT_TRADITIONAL : CT_VIRTUAL;
+ }
+ return CT_MYSTERY;
}
+
}