diff --git a/README.rst b/README.rst index abe136a..f52ba09 100644 --- a/README.rst +++ b/README.rst @@ -127,27 +127,38 @@ backend: By default, the backend is 'auto', i.e. it is determined from the backend setting in the world's world.mt file (if found). -geometry : - (see below, under 'centergeometry') +centergeometry : + (see below, under 'geometry') cornergeometry : - (see below, under 'centergeometry') + (see below, under 'geometry') -centergeometry : +geometry : Limit the part of the world that is included in the map. has one of the formats: - x[<+|-xoffset><+|-yoffset>] + x[<+|-xoffset><+|-yoffset>] (dimensions & corner) - :+width+height + ,+width+height (corner & dimensions) - For cornergeometry, the offsets will be at the lower-left - corner of the image (offsets increase from left to right, - and from bottom to top). + ,:widthxheight (center & dimensions) - For centergeometry, the offsets will be in the center of - the image. + ,:, + + The old/original format is also supported: + + :+width+height (corner & dimensions) + + For 'cornergeometry', the offsets ([xy]offset or [xy]center) will + be at the lower-left corner of the image (offsets increase from left + to right, and from bottom to top). + + For 'centergeometry', the offsets ([xy]offset or [xy]center) will be + in the center of the image. + + For plain 'geometry', the offsets will be at the corner, or in + the center, depending on the geometry format. If the offsets are not specified (with the first format), the map is centered on the center of the world. @@ -155,21 +166,26 @@ centergeometry : By default, the geometry has pixel granularity, and a map of exactly the requested size is generated. - Only if the *first* geometry option on the command-line is - `--geometry`, then for compatibility, the old behavior - is default instead (i.e. block granularity, and a smaller - map if possible). Block granularity is also enabled when - the obsolete option '--forcegeometry' is found first. + *Compatibility mode*: + + If the *first* geometry-related option on the command-line + is `--geometry`, *and* if the old format is used, then for + compatibility, the old behavior is default instead (i.e. + block granularity, and a smaller map if possible). Block + granularity is also enabled when the obsolete (and otherwise + undocumented) option '--forcegeometry' is found first. Examples: `--geometry 10x10-5-5` + `--geometry 100,100:500,1000` + `--cornergeometry 50x50+100+100` `--centergeometry 1100x1300+1000-500` - `--centergeometry 1100x1300` + `--geometry 1100x1300` geometrymode pixel,block,fixed,shrink: Specify how the geometry should be interpreted. One or @@ -199,6 +215,11 @@ geometrymode fixed: Generate a map of the requested geometry, even if part or all of it would be empty. + *NOTE*: If this flag is used, and no actual geometry is + specified, this would result in a maximum-size map (65536 + x 65536), which is currently not possible, and will fail, + due to a bug in the drawing library. + geometrymode shrink: Generate a map of at most the requested geometry. Shrink it to the smallest possible size that still includes the diff --git a/TileGenerator.cpp b/TileGenerator.cpp index bf9fbe9..c04c79a 100644 --- a/TileGenerator.cpp +++ b/TileGenerator.cpp @@ -259,40 +259,37 @@ void TileGenerator::enableProgressIndicator(void) progressIndicator = true; } -void TileGenerator::setGeometry(int x, int y, int w, int h) +void TileGenerator::setGeometry(const NodeCoord &corner1, const NodeCoord &corner2) { - if (x > 0) { - m_reqXMin = x / 16; + if (corner1.x > 0) { + m_reqXMin = corner1.x / 16; } else { - m_reqXMin = (x - 15) / 16; + m_reqXMin = (corner1.x - 15) / 16; } - if (y > 0) { - m_reqZMin = y / 16; + if (corner1.y > 0) { + m_reqZMin = corner1.y / 16; } else { - m_reqZMin = (y - 15) / 16; + m_reqZMin = (corner1.y - 15) / 16; } - m_mapXStartNodeOffset = x - m_reqXMin * 16; - m_mapYEndNodeOffset = m_reqZMin * 16 - y; + m_mapXStartNodeOffset = corner1.x - m_reqXMin * 16; + m_mapYEndNodeOffset = m_reqZMin * 16 - corner1.y; - int x2 = x + w - 1; - int y2 = y + h - 1; - - if (x2 > 0) { - m_reqXMax = x2 / 16; + if (corner2.x > 0) { + m_reqXMax = corner2.x / 16; } else { - m_reqXMax = (x2 - 15) / 16; + m_reqXMax = (corner2.x - 15) / 16; } - if (y2 > 0) { - m_reqZMax = y2 / 16; + if (corner2.y > 0) { + m_reqZMax = corner2.y / 16; } else { - m_reqZMax = (y2 - 15) / 16; + m_reqZMax = (corner2.y - 15) / 16; } - m_mapXEndNodeOffset = x2 - (m_reqXMax * 16 + 15); - m_mapYStartNodeOffset = (m_reqZMax * 16 + 15) - y2; + m_mapXEndNodeOffset = corner2.x - (m_reqXMax * 16 + 15); + m_mapYStartNodeOffset = (m_reqZMax * 16 + 15) - corner2.y; } void TileGenerator::setMinY(int y) diff --git a/TileGenerator.h b/TileGenerator.h index 457e88c..dcbb74b 100644 --- a/TileGenerator.h +++ b/TileGenerator.h @@ -66,7 +66,7 @@ public: void setDrawScale(bool drawScale); void setDrawAlpha(bool drawAlpha); void setShading(bool shading); - void setGeometry(int x, int y, int w, int h); + void setGeometry(const NodeCoord &corner1, const NodeCoord &corner2); void setMinY(int y); void setMaxY(int y); void setShrinkGeometry(bool shrink); diff --git a/mapper.cpp b/mapper.cpp index 8631d6f..a994e4b 100644 --- a/mapper.cpp +++ b/mapper.cpp @@ -24,6 +24,27 @@ using namespace std; #define OPT_SQLITE_CACHEWORLDROW 0x81 #define OPT_PROGRESS_INDICATOR 0x82 +class FuzzyBool { +private: + int m_value; + FuzzyBool(int i) : m_value(i) {} +public: + FuzzyBool() : m_value(0) {} + FuzzyBool(bool b) : m_value(b ? Yes.m_value : No.m_value) {} + static const FuzzyBool Yes; + static const FuzzyBool Maybe; + static const FuzzyBool No; +inline friend bool operator==(FuzzyBool f1, FuzzyBool f2) { return f1.m_value == f2.m_value; } +inline friend bool operator!=(FuzzyBool f1, FuzzyBool f2) { return f1.m_value != f2.m_value; } +inline friend bool operator>=(FuzzyBool f1, FuzzyBool f2) { return f1.m_value >= f2.m_value; } +inline friend bool operator<=(FuzzyBool f1, FuzzyBool f2) { return f1.m_value <= f2.m_value; } +inline friend bool operator<(FuzzyBool f1, FuzzyBool f2) { return f1.m_value < f2.m_value; } +inline friend bool operator>(FuzzyBool f1, FuzzyBool f2) { return f1.m_value < f2.m_value; } +}; +const FuzzyBool FuzzyBool::Yes = 1; +const FuzzyBool FuzzyBool::Maybe = 0; +const FuzzyBool FuzzyBool::No = -1; + void usage() { const char *usage_text = "minetestmapper [options]\n" @@ -45,9 +66,14 @@ void usage() " --max-y \n" " --backend <" USAGE_DATABASES ">\n" " --geometry \n" + "\t(Warning: has a compatibility mode - see README.rst)\n" " --cornergeometry \n" " --centergeometry \n" " --geometrymode pixel,block,fixed,shrink\n" + "\tpixel: interpret geometry as pixel-accurate\n" + "\tblock: round geometry away from zero, to entire map blocks (16 nodes)\n" + "\tfixed: generate a map of exactly the requested geometry\n" + "\tshrink: generate a smaller map if possible\n" #if USE_SQLITE3 " --sqlite-cacheworldrow\n" #endif @@ -59,8 +85,17 @@ void usage() "\t'#000' or '#000000' (RGB)\n" "\t'#0000' or '#0000000' (ARGB - usable if an alpha value is allowed)\n" "Geometry formats:\n" - "\tx[+|-+|-]\n" - "\t:++\n"; + "\tx[+|-+|-] (dimensions and corner)\n" + "\t,++ (corner and dimensions)\n" + "\t,:x (center and dimensions)\n" + "\t,:, (corners of area)\n" + "\tOriginal/legacy format - see note under '--geometry' option:\n" + "\t:++ (corner and dimensions)\n" + "X and Y coordinate formats:\n" + "\t[+-] (node +/- )\n" + "\t[+-]#[] (node in block +/- )\n" + "\t[+-].[] (node +/- (b * 16 + n))\n" + ; std::cout << usage_text; } @@ -125,6 +160,313 @@ void parseColorsFile(TileGenerator &generator, const string &input, string color } } +// is: stream to read from +// coord: set to coordinate value that was read +// isBlockCoord: set to true if the coordinate read was a block coordinate +// wildcard: if non-zero, accept '*' as a coordinate, and return this value instead. +// (suggested values for 'wildcard': INT_MIN or INT_MAX) +// +// Accepted coordinate syntax: +// [+-]: node coordinate: node +/- n +// [+-]#: block coordinate: block +/- b (isBlockCoord will be set to true) +// [+-]#: node coordinate: node in block +/- +// [+-].: node coordinate: node +/- (b * 16 + n) +// As a special feature, double signs are also supported. E.g.: +// +-3 +// Which allows shell command-lines like the following +// ${width}x${height}+$xoffs+$yoffs +// (which otherwise require special measures to cope with xoffs or yoffs being negative...) +// Other uses of this feature are left as an excercise to the reader. +// Hint: --3.5 is *not* the same as 3.5 +static bool parseNodeCoordinate(istream &is, int &coord, bool &isBlockCoord, int wildcard) +{ + char c; + int i; + char s; + + s = c = is.peek(); + if (c == '*') { + if (wildcard) { + i = wildcard; + is.ignore(1); + } + else { + is >> coord; // Set stream status to failed + } + } + else { + wildcard = 0; // not processing a wildcard now + if (s == '-' || s == '+') + is.ignore(1); + else + s = '+'; + is >> i; + if (s == '-') + i = -i; + } + if (is.fail()) + return false; + coord = i; + isBlockCoord = false; + if (is.eof()) + return true; + + // Check if this is a block number, and so: if it has a node number. + c = is.peek(); + if (c == '#' || c == '.') { + // coordinate read was a block number + is.ignore(1); + if (wildcard) { + return false; // wildcards are generic + } + else if (isdigit(is.peek())) { + // has a node number / offset + is >> i; + if (!is.fail()) { + if (c == '.' && s == '-') { + // Using '.', the node number has same sign as block number + // Using '#', the node number is always positive + // i.e. -1#1 is: node #1 in block -1 (i.e. node -16 + 1 = -15) + // i.e. -1.1 is: 1 block and 1 node in negative direction (i.e. node 16 - 1 = -17) + i = -i; + } + coord = coord * 16 + i; + } + } + else { + // No node number / offset + isBlockCoord = true; + } + } + return (!is.fail()); +} + +static bool parseCoordinates(istream &is, NodeCoord &coord, int n, int wildcard = 0, char separator = ',') +{ + bool result; + result = true; + NodeCoord tempCoord; + for (int i = 0; result && i < n; i++) { + if (i && separator) { + char c; + is >> c; + if (c != separator) { + result = false; + break; + } + } + result = parseNodeCoordinate(is, tempCoord.dimension[i], tempCoord.isBlock[i], wildcard); + } + if (result) + coord = tempCoord; + return result; +} + +static void convertBlockToNodeCoordinates(NodeCoord &coord, int offset, int n) +{ + for (int i = 0; i < n; i++) { + if (coord.isBlock[i]) { + coord.dimension[i] = coord.dimension[i] * 16 + offset; + coord.isBlock[i] = false; + } + } +} + +static void convertBlockToNodeCoordinates(NodeCoord &coord1, NodeCoord &coord2, int n) +{ + for (int i = 0; i < n; i++) { + int c1 = coord1.isBlock[i] ? coord1.dimension[i] * 16 : coord1.dimension[i]; + int c2 = coord2.isBlock[i] ? coord2.dimension[i] * 16 + 15 : coord2.dimension[i]; + if (c1 > c2) { + c1 = coord1.isBlock[i] ? coord1.dimension[i] * 16 + 15 : coord1.dimension[i]; + c2 = coord2.isBlock[i] ? coord2.dimension[i] * 16 : coord2.dimension[i]; + } + coord1.dimension[i] = c1; + coord2.dimension[i] = c2; + coord1.isBlock[i] = false; + coord2.isBlock[i] = false; + } +} + +static void convertCenterToCornerCoordinates(NodeCoord &coord, NodeCoord &dimensions, int n) +{ + // This results in a slight bias to the negative side. + // i.e.: 0,0:2x2 will be -1,-1 .. 0,0 and not 0,0 .. 1,1 + // The advantage is that e.g. 0#,0#:16x16 selects the 16x16 area that is block 0: + // 0#,0#:16x16 -> 0,0:15,15 + // With a bias to the positive side, that would be: + // 0#,0#:16x16 -> 1,1:16,16 + // Which is counter-intuitive by itself (IMHO :-) + for (int i = 0; i < n; i++) { + if (dimensions.dimension[i] < 0) + coord.dimension[i] += -dimensions.dimension[i] / 2; + else + coord.dimension[i] -= dimensions.dimension[i] / 2; + } +} + +static void convertDimensionToCornerCoordinates(NodeCoord &coord1, NodeCoord &coord2, NodeCoord &dimensions, int n) +{ + for (int i = 0; i < n; i++) { + if (dimensions.dimension[i] < 0) + coord2.dimension[i] = coord1.dimension[i] + dimensions.dimension[i] + 1; + else + coord2.dimension[i] = coord1.dimension[i] + dimensions.dimension[i] - 1; + } +} + +static void orderCoordinateDimensions(NodeCoord &coord1, NodeCoord &coord2, int n) +{ + for (int i = 0; i < n; i++) + if (coord1.dimension[i] > coord2.dimension[i]) { + int temp = coord1.dimension[i]; + coord1.dimension[i] = coord2.dimension[i]; + coord2.dimension[i] = temp; + } +} + + +// Parse the following geometry formats: +// x[++] +// (dimensions, and position) +// (if x and y are omitted, they default to -w/2 and -h/2) +// ,:, +// (2 corners of the area) +// ,:x +// (center of the area, and dimensions) +// [,:]++ +// (corner of the area, and dimensions) +static bool parseGeometry(istream &is, NodeCoord &coord1, NodeCoord &coord2, NodeCoord &dimensions, bool &legacy, bool ¢ered, int n, FuzzyBool expectDimensions, int wildcard = 0) +{ + int pos; + pos = is.tellg(); + legacy = false; + + for (int i = 0; i < n; i++) { + coord1.dimension[i] = NodeCoord::Invalid; + coord2.dimension[i] = NodeCoord::Invalid; + dimensions.dimension[i] = NodeCoord::Invalid; + } + + if (expectDimensions >= FuzzyBool::Maybe && parseCoordinates(is, dimensions, n, 0, 'x')) { + convertBlockToNodeCoordinates(dimensions, 0, n); + // x[++] + if (is.eof()) { + centered = true; + for (int i = 0; i < n; i++) { + coord1.dimension[i] = 0; + coord1.isBlock[i] = false; + } + return (is.eof() || is.peek() == ' ' || is.peek() == '\t'); + } + else { + centered = false; + if (parseCoordinates(is, coord1, n, 0, '\0')) { + convertBlockToNodeCoordinates(coord1, 0, n); + return (is.eof() || is.peek() == ' ' || is.peek() == '\t'); + } + else + return false; + } + } + + is.clear(); + is.seekg(pos); + if (wildcard) { + coord1.x = coord1.y = coord1.z = 0; + } + if (parseCoordinates(is, coord1, n, wildcard, ',')) { + if (expectDimensions == FuzzyBool::No || (expectDimensions == FuzzyBool::Maybe && (is.eof() || is.peek() == ' ' || is.peek() == '\t'))) { + // Just coordinates were specified + centered = false; + return (is.eof() || is.peek() == ' ' || is.peek() == '\t'); + } + else if (wildcard && (coord1.x == wildcard || coord1.y == wildcard || coord1.z == wildcard)) { + // wildcards are only allowed for plain coordinates (i.e. no dimensions) + return false; + } + else if (is.peek() == ':') { + is.ignore(1); + pos = is.tellg(); + if (parseCoordinates(is, coord2, n, 0, ',')) { + // ,:, + centered = false; + convertBlockToNodeCoordinates(coord1, coord2, n); + return (is.eof() || is.peek() == ' ' || is.peek() == '\t'); + } + is.clear(); + is.seekg(pos); + if (parseCoordinates(is, dimensions, n, 0, 'x')) { + // ,:x + // (x,y is the center of the area by default) + centered = true; + convertBlockToNodeCoordinates(coord1, 8, n); + convertBlockToNodeCoordinates(dimensions, 0, n); + return (is.eof() || is.peek() == ' ' || is.peek() == '\t'); + } + else { + return false; + } + } + else { + // ,++ + centered = false; + if (parseCoordinates(is, dimensions, n, 0, '\0')) { + convertBlockToNodeCoordinates(coord1, 0, n); + convertBlockToNodeCoordinates(dimensions, 0, n); + return (is.eof() || is.peek() == ' ' || is.peek() == '\t'); + } + else { + return false; + } + } + } + + is.clear(); + is.seekg(pos); + if (parseCoordinates(is, coord1, n, 0, ':')) { + // :++ + legacy = true; + centered = false; + if (parseCoordinates(is, dimensions, n, 0, '\0')) { + convertBlockToNodeCoordinates(coord1, 0, n); + convertBlockToNodeCoordinates(dimensions, 0, n); + return (is.eof() || is.peek() == ' ' || is.peek() == '\t'); + } + return false; + } + + return false; +} + +static bool parseMapGeometry(istream &is, NodeCoord &coord1, NodeCoord &coord2, bool &legacy, FuzzyBool interpretAsCenter) +{ + NodeCoord dimensions; + bool centered; + + bool result = parseGeometry(is, coord1, coord2, dimensions, legacy, centered, 2, FuzzyBool::Yes); + + if (result) { + bool haveCoord2 = coord2.dimension[0] != NodeCoord::Invalid + && coord2.dimension[1] != NodeCoord::Invalid; + bool haveDimensions = dimensions.dimension[0] != NodeCoord::Invalid + && dimensions.dimension[1] != NodeCoord::Invalid; + if (!haveCoord2 && haveDimensions) { + // Convert coord1 + dimensions to coord1 + coord2. + // First, if coord1 must be interpreted as center of the area, adjust it to be a corner + if ((centered && interpretAsCenter == FuzzyBool::Maybe) || interpretAsCenter == FuzzyBool::Yes) + convertCenterToCornerCoordinates(coord1, dimensions, 2); + convertDimensionToCornerCoordinates(coord1, coord2, dimensions, 2); + } + else if (!haveCoord2 || haveDimensions) { + return false; + } + orderCoordinateDimensions(coord1, coord2, 2); + } + + return result; +} + int main(int argc, char *argv[]) { static struct option long_options[] = @@ -284,10 +626,16 @@ int main(int argc, char *argv[]) case 'T': { istringstream origin; origin.str(optarg); - int x, y; - char c; - origin >> x >> c >> y; - if (origin.fail() || (c != ':' && c != ',')) { + NodeCoord coord; + if (parseCoordinates(origin, coord, 2, 0, ',')) { + convertBlockToNodeCoordinates(coord, 8, 2); + generator.setTileOrigin(coord.x, coord.y); + } + else if (origin.str(optarg), parseCoordinates(origin, coord, 2, 0, ':')) { + convertBlockToNodeCoordinates(coord, 8, 2); + generator.setTileOrigin(coord.x, coord.y); + } + else { if (string("center-world") == optarg) generator.setTileOrigin(TILECENTER_IS_WORLDCENTER, TILECENTER_IS_WORLDCENTER); else if (string("center-map") == optarg) @@ -298,9 +646,6 @@ int main(int argc, char *argv[]) exit(1); } } - else { - generator.setTileOrigin(x, y); - } } break; case 'G': @@ -349,9 +694,24 @@ int main(int argc, char *argv[]) foundGeometrySpec = true; break; case 'g': { + istringstream iss; + iss.str(optarg); + NodeCoord coord1; + NodeCoord coord2; + bool legacy; + FuzzyBool center = FuzzyBool::Maybe; + if (long_options[option_index].name[0] == 'c' && long_options[option_index].name[1] == 'e') + center = FuzzyBool::Yes; + if (long_options[option_index].name[0] == 'c' && long_options[option_index].name[1] == 'o') + center = FuzzyBool::No; + if (!parseMapGeometry(iss, coord1, coord2, legacy, center)) { + std::cerr << "Invalid geometry specification '" << optarg << "'" << std::endl; + usage(); + exit(1); + } // Set defaults if (!foundGeometrySpec) { - if (long_options[option_index].name[0] == 'g') { + if (long_options[option_index].name[0] == 'g' && legacy) { // Compatibility when using the option 'geometry' generator.setBlockGeometry(true); generator.setShrinkGeometry(true); @@ -369,46 +729,7 @@ int main(int argc, char *argv[]) generator.setShrinkGeometry(false); setFixedOrShrinkGeometry = true; } - - istringstream iss; - iss.str(optarg); - int p1, p2, p3, p4; - char c; - iss >> p1 >> c >> p2; - if (!iss.fail() && c == 'x' && iss.eof()) { - p3 = -(p1 / 2); - p4 = -(p2 / 2); - } - else { - char s3, s4; - iss >> s3 >> p3 >> s4 >> p4; - // accept +-23 as well (for ease of use) - if ((s3 != '+' && s3 != '-') || (s4 != '+' && s4 != '-')) - c = 0; // Causes an 'invalid geometry' message - if (s3 == '-') p3 = -p3; - if (s4 == '-') p4 = -p4; - if (long_options[option_index].name[0] == 'c' - && long_options[option_index].name[1] == 'e') { - // option 'centergeometry' - p3 -= p1 / 2; - p4 -= p2 / 2; - } - } - if (iss.fail() || (c != ':' && c != 'x')) { - std::cerr << "Invalid geometry specification '" << optarg << "'" << std::endl; - usage(); - exit(1); - } - if ((c == ':' && (p3 < 1 || p4 < 1)) - || (c == 'x' && (p1 < 1 || p2 < 1))) { - std::cerr << "Invalid geometry (width and/or heigth is zero or negative)" << std::endl; - usage(); - exit(1); - } - if (c == ':') - generator.setGeometry(p1, p2, p3, p4); - if (c == 'x') - generator.setGeometry(p3, p4, p1, p2); + generator.setGeometry(coord1, coord2); foundGeometrySpec = true; } break;