diff options
Diffstat (limited to 'libtiled/mapreader.cpp')
| -rw-r--r-- | libtiled/mapreader.cpp | 772 |
1 files changed, 772 insertions, 0 deletions
diff --git a/libtiled/mapreader.cpp b/libtiled/mapreader.cpp new file mode 100644 index 0000000..ca8e19c --- /dev/null +++ b/libtiled/mapreader.cpp @@ -0,0 +1,772 @@ +/* + * mapreader.cpp + * Copyright 2008-2010, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl> + * Copyright 2010, Jeff Bland <jksb@member.fsf.org> + * Copyright 2010, Dennis Honeyman <arcticuno@gmail.com> + * + * This file is part of libtiled. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "mapreader.h" + +#include "compression.h" +#include "gidmapper.h" +#include "objectgroup.h" +#include "map.h" +#include "mapobject.h" +#include "tile.h" +#include "tilelayer.h" +#include "tileset.h" + +#include <QCoreApplication> +#include <QDebug> +#include <QDir> +#include <QFileInfo> +#include <QXmlStreamReader> + +using namespace Tiled; +using namespace Tiled::Internal; + +namespace Tiled { +namespace Internal { + +class MapReaderPrivate +{ + Q_DECLARE_TR_FUNCTIONS(MapReader) + +public: + MapReaderPrivate(MapReader *mapReader): + p(mapReader), + mMap(0), + mReadingExternalTileset(false) + {} + + Map *readMap(QIODevice *device, const QString &path); + Tileset *readTileset(QIODevice *device, const QString &path); + + bool openFile(QFile *file); + + QString errorString() const; + +private: + void readUnknownElement(); + + Map *readMap(); + + Tileset *readTileset(); + void readTilesetTile(Tileset *tileset); + void readTilesetImage(Tileset *tileset); + + TileLayer *readLayer(); + void readLayerData(TileLayer *tileLayer); + void decodeBinaryLayerData(TileLayer *tileLayer, + const QStringRef &text, + const QStringRef &compression); + void decodeCSVLayerData(TileLayer *tileLayer, const QString &text); + + /** + * Returns the cell for the given global tile ID. Errors are raised with + * the QXmlStreamReader. + * + * @param gid the global tile ID + * @return the cell data associated with the given global tile ID, or an + * empty cell if not found + */ + Cell cellForGid(uint gid); + + ObjectGroup *readObjectGroup(); + MapObject *readObject(); + QPolygonF readPolygon(); + + Properties readProperties(); + void readProperty(Properties *properties); + + MapReader *p; + + QString mError; + QString mPath; + Map *mMap; + GidMapper mGidMapper; + bool mReadingExternalTileset; + + QXmlStreamReader xml; +}; + +} // namespace Internal +} // namespace Tiled + +Map *MapReaderPrivate::readMap(QIODevice *device, const QString &path) +{ + mError.clear(); + mPath = path; + Map *map = 0; + + xml.setDevice(device); + + if (xml.readNextStartElement() && xml.name() == "map") { + map = readMap(); + } else { + xml.raiseError(tr("Not a map file.")); + } + + mGidMapper.clear(); + return map; +} + +Tileset *MapReaderPrivate::readTileset(QIODevice *device, const QString &path) +{ + mError.clear(); + mPath = path; + Tileset *tileset = 0; + mReadingExternalTileset = true; + + xml.setDevice(device); + + if (xml.readNextStartElement() && xml.name() == "tileset") + tileset = readTileset(); + else + xml.raiseError(tr("Not a tileset file.")); + + mReadingExternalTileset = false; + return tileset; +} + +QString MapReaderPrivate::errorString() const +{ + if (!mError.isEmpty()) { + return mError; + } else { + return tr("%3\n\nLine %1, column %2") + .arg(xml.lineNumber()) + .arg(xml.columnNumber()) + .arg(xml.errorString()); + } +} + +bool MapReaderPrivate::openFile(QFile *file) +{ + if (!file->exists()) { + mError = tr("File not found: %1").arg(file->fileName()); + return false; + } else if (!file->open(QFile::ReadOnly | QFile::Text)) { + mError = tr("Unable to read file: %1").arg(file->fileName()); + return false; + } + + return true; +} + +void MapReaderPrivate::readUnknownElement() +{ + qDebug() << "Unknown element (fixme):" << xml.name(); + xml.skipCurrentElement(); +} + +Map *MapReaderPrivate::readMap() +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "map"); + + const QXmlStreamAttributes atts = xml.attributes(); + const int mapWidth = + atts.value(QLatin1String("width")).toString().toInt(); + const int mapHeight = + atts.value(QLatin1String("height")).toString().toInt(); + const int tileWidth = + atts.value(QLatin1String("tilewidth")).toString().toInt(); + const int tileHeight = + atts.value(QLatin1String("tileheight")).toString().toInt(); + + const QString orientationString = + atts.value(QLatin1String("orientation")).toString(); + const Map::Orientation orientation = + orientationFromString(orientationString); + + if (orientation == Map::Unknown) { + xml.raiseError(tr("Unsupported map orientation: \"%1\"") + .arg(orientationString)); + } + + mMap = new Map(orientation, mapWidth, mapHeight, tileWidth, tileHeight); + + while (xml.readNextStartElement()) { + if (xml.name() == "properties") + mMap->mergeProperties(readProperties()); + else if (xml.name() == "tileset") + mMap->addTileset(readTileset()); + else if (xml.name() == "layer") + mMap->addLayer(readLayer()); + else if (xml.name() == "objectgroup") + mMap->addLayer(readObjectGroup()); + else + readUnknownElement(); + } + + // Clean up in case of error + if (xml.hasError()) { + // The tilesets are not owned by the map + qDeleteAll(mMap->tilesets()); + + delete mMap; + mMap = 0; + } + + return mMap; +} + +Tileset *MapReaderPrivate::readTileset() +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "tileset"); + + const QXmlStreamAttributes atts = xml.attributes(); + const QString source = atts.value(QLatin1String("source")).toString(); + const uint firstGid = + atts.value(QLatin1String("firstgid")).toString().toUInt(); + + Tileset *tileset = 0; + + if (source.isEmpty()) { // Not an external tileset + const QString name = + atts.value(QLatin1String("name")).toString(); + const int tileWidth = + atts.value(QLatin1String("tilewidth")).toString().toInt(); + const int tileHeight = + atts.value(QLatin1String("tileheight")).toString().toInt(); + const int tileSpacing = + atts.value(QLatin1String("spacing")).toString().toInt(); + const int margin = + atts.value(QLatin1String("margin")).toString().toInt(); + + if (tileWidth <= 0 || tileHeight <= 0 + || (firstGid == 0 && !mReadingExternalTileset)) { + xml.raiseError(tr("Invalid tileset parameters for tileset" + " '%1'").arg(name)); + } else { + tileset = new Tileset(name, tileWidth, tileHeight, + tileSpacing, margin); + + while (xml.readNextStartElement()) { + if (xml.name() == "tile") { + readTilesetTile(tileset); + } else if (xml.name() == "tileoffset") { + const QXmlStreamAttributes oa = xml.attributes(); + int x = oa.value(QLatin1String("x")).toString().toInt(); + int y = oa.value(QLatin1String("y")).toString().toInt(); + tileset->setTileOffset(QPoint(x, y)); + xml.skipCurrentElement(); + } else if (xml.name() == "properties") { + tileset->mergeProperties(readProperties()); + } else if (xml.name() == "image") { + readTilesetImage(tileset); + } else { + readUnknownElement(); + } + } + } + } else { // External tileset + const QString absoluteSource = p->resolveReference(source, mPath); + QString error; + tileset = p->readExternalTileset(absoluteSource, &error); + + if (!tileset) { + xml.raiseError(tr("Error while loading tileset '%1': %2") + .arg(absoluteSource, error)); + } + + xml.skipCurrentElement(); + } + + if (tileset && !mReadingExternalTileset) + mGidMapper.insert(firstGid, tileset); + + return tileset; +} + +void MapReaderPrivate::readTilesetTile(Tileset *tileset) +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "tile"); + + const QXmlStreamAttributes atts = xml.attributes(); + const int id = atts.value(QLatin1String("id")).toString().toInt(); + + if (id < 0 || id >= tileset->tileCount()) { + xml.raiseError(tr("Invalid tile ID: %1").arg(id)); + return; + } + + // TODO: Add support for individual tiles (then it needs to be added here) + + while (xml.readNextStartElement()) { + if (xml.name() == "properties") { + Tile *tile = tileset->tileAt(id); + tile->mergeProperties(readProperties()); + } else { + readUnknownElement(); + } + } +} + +void MapReaderPrivate::readTilesetImage(Tileset *tileset) +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "image"); + + const QXmlStreamAttributes atts = xml.attributes(); + QString source = atts.value(QLatin1String("source")).toString(); + QString trans = atts.value(QLatin1String("trans")).toString(); + + if (!trans.isEmpty()) { + if (!trans.startsWith(QLatin1Char('#'))) + trans.prepend(QLatin1Char('#')); + tileset->setTransparentColor(QColor(trans)); + } + + source = p->resolveReference(source, mPath); + + // Set the width that the tileset had when the map was saved + const int width = atts.value(QLatin1String("width")).toString().toInt(); + mGidMapper.setTilesetWidth(tileset, width); + + const QImage tilesetImage = p->readExternalImage(source); + if (!tileset->loadFromImage(tilesetImage, source)) + xml.raiseError(tr("Error loading tileset image:\n'%1'").arg(source)); + + xml.skipCurrentElement(); +} + +static void readLayerAttributes(Layer *layer, + const QXmlStreamAttributes &atts) +{ + const QStringRef opacityRef = atts.value(QLatin1String("opacity")); + const QStringRef visibleRef = atts.value(QLatin1String("visible")); + + bool ok; + const float opacity = opacityRef.toString().toFloat(&ok); + if (ok) + layer->setOpacity(opacity); + + const int visible = visibleRef.toString().toInt(&ok); + if (ok) + layer->setVisible(visible); +} + +TileLayer *MapReaderPrivate::readLayer() +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "layer"); + + const QXmlStreamAttributes atts = xml.attributes(); + const QString name = atts.value(QLatin1String("name")).toString(); + const int x = atts.value(QLatin1String("x")).toString().toInt(); + const int y = atts.value(QLatin1String("y")).toString().toInt(); + const int width = atts.value(QLatin1String("width")).toString().toInt(); + const int height = atts.value(QLatin1String("height")).toString().toInt(); + + TileLayer *tileLayer = new TileLayer(name, x, y, width, height); + readLayerAttributes(tileLayer, atts); + + while (xml.readNextStartElement()) { + if (xml.name() == "properties") + tileLayer->mergeProperties(readProperties()); + else if (xml.name() == "data") + readLayerData(tileLayer); + else + readUnknownElement(); + } + + return tileLayer; +} + +void MapReaderPrivate::readLayerData(TileLayer *tileLayer) +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "data"); + + const QXmlStreamAttributes atts = xml.attributes(); + QStringRef encoding = atts.value(QLatin1String("encoding")); + QStringRef compression = atts.value(QLatin1String("compression")); + + int x = 0; + int y = 0; + + while (xml.readNext() != QXmlStreamReader::Invalid) { + if (xml.isEndElement()) + break; + else if (xml.isStartElement()) { + if (xml.name() == QLatin1String("tile")) { + if (y >= tileLayer->height()) { + xml.raiseError(tr("Too many <tile> elements")); + continue; + } + + const QXmlStreamAttributes atts = xml.attributes(); + uint gid = atts.value(QLatin1String("gid")).toString().toUInt(); + tileLayer->setCell(x, y, cellForGid(gid)); + + x++; + if (x >= tileLayer->width()) { + x = 0; + y++; + } + + xml.skipCurrentElement(); + } else { + readUnknownElement(); + } + } else if (xml.isCharacters() && !xml.isWhitespace()) { + if (encoding == QLatin1String("base64")) { + decodeBinaryLayerData(tileLayer, + xml.text(), + compression); + } else if (encoding == QLatin1String("csv")) { + decodeCSVLayerData(tileLayer, xml.text().toString()); + } else { + xml.raiseError(tr("Unknown encoding: %1") + .arg(encoding.toString())); + continue; + } + } + } +} + +void MapReaderPrivate::decodeBinaryLayerData(TileLayer *tileLayer, + const QStringRef &text, + const QStringRef &compression) +{ +#if QT_VERSION < 0x040800 + const QString textData = QString::fromRawData(text.unicode(), text.size()); + const QByteArray latin1Text = textData.toLatin1(); +#else + const QByteArray latin1Text = text.toLatin1(); +#endif + QByteArray tileData = QByteArray::fromBase64(latin1Text); + const int size = (tileLayer->width() * tileLayer->height()) * 4; + + if (compression == QLatin1String("zlib") + || compression == QLatin1String("gzip")) { + tileData = decompress(tileData, size); + } else if (!compression.isEmpty()) { + xml.raiseError(tr("Compression method '%1' not supported") + .arg(compression.toString())); + return; + } + + if (size != tileData.length()) { + xml.raiseError(tr("Corrupt layer data for layer '%1'") + .arg(tileLayer->name())); + return; + } + + const unsigned char *data = + reinterpret_cast<const unsigned char*>(tileData.constData()); + int x = 0; + int y = 0; + + for (int i = 0; i < size - 3; i += 4) { + const uint gid = data[i] | + data[i + 1] << 8 | + data[i + 2] << 16 | + data[i + 3] << 24; + + tileLayer->setCell(x, y, cellForGid(gid)); + + x++; + if (x == tileLayer->width()) { + x = 0; + y++; + } + } +} + +void MapReaderPrivate::decodeCSVLayerData(TileLayer *tileLayer, const QString &text) +{ + QString trimText = text.trimmed(); + QStringList tiles = trimText.split(QLatin1Char(',')); + + if (tiles.length() != tileLayer->width() * tileLayer->height()) { + xml.raiseError(tr("Corrupt layer data for layer '%1'") + .arg(tileLayer->name())); + return; + } + + for (int y = 0; y < tileLayer->height(); y++) { + for (int x = 0; x < tileLayer->width(); x++) { + bool conversionOk; + const uint gid = tiles.at(y * tileLayer->width() + x) + .toUInt(&conversionOk); + if (!conversionOk) { + xml.raiseError( + tr("Unable to parse tile at (%1,%2) on layer '%3'") + .arg(x + 1).arg(y + 1).arg(tileLayer->name())); + return; + } + tileLayer->setCell(x, y, cellForGid(gid)); + } + } +} + +Cell MapReaderPrivate::cellForGid(uint gid) +{ + bool ok; + const Cell result = mGidMapper.gidToCell(gid, ok); + + if (!ok) { + if (mGidMapper.isEmpty()) + xml.raiseError(tr("Tile used but no tilesets specified")); + else + xml.raiseError(tr("Invalid tile: %1").arg(gid)); + } + + return result; +} + +ObjectGroup *MapReaderPrivate::readObjectGroup() +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "objectgroup"); + + const QXmlStreamAttributes atts = xml.attributes(); + const QString name = atts.value(QLatin1String("name")).toString(); + const int x = atts.value(QLatin1String("x")).toString().toInt(); + const int y = atts.value(QLatin1String("y")).toString().toInt(); + const int width = atts.value(QLatin1String("width")).toString().toInt(); + const int height = atts.value(QLatin1String("height")).toString().toInt(); + + ObjectGroup *objectGroup = new ObjectGroup(name, x, y, width, height); + readLayerAttributes(objectGroup, atts); + + const QString color = atts.value(QLatin1String("color")).toString(); + if (!color.isEmpty()) + objectGroup->setColor(color); + + while (xml.readNextStartElement()) { + if (xml.name() == "object") + objectGroup->addObject(readObject()); + else if (xml.name() == "properties") + objectGroup->mergeProperties(readProperties()); + else + readUnknownElement(); + } + + return objectGroup; +} + +static QPointF pixelToTileCoordinates(Map *map, int x, int y) +{ + const int tileHeight = map->tileHeight(); + const int tileWidth = map->tileWidth(); + + if (map->orientation() == Map::Isometric) { + // Isometric needs special handling, since the pixel values are based + // solely on the tile height. + return QPointF((qreal) x / tileHeight, + (qreal) y / tileHeight); + } else { + return QPointF((qreal) x / tileWidth, + (qreal) y / tileHeight); + } +} + +MapObject *MapReaderPrivate::readObject() +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "object"); + + const QXmlStreamAttributes atts = xml.attributes(); + const QString name = atts.value(QLatin1String("name")).toString(); + const uint gid = atts.value(QLatin1String("gid")).toString().toUInt(); + const int x = atts.value(QLatin1String("x")).toString().toInt(); + const int y = atts.value(QLatin1String("y")).toString().toInt(); + const int width = atts.value(QLatin1String("width")).toString().toInt(); + const int height = atts.value(QLatin1String("height")).toString().toInt(); + const QString type = atts.value(QLatin1String("type")).toString(); + + const QPointF pos = pixelToTileCoordinates(mMap, x, y); + const QPointF size = pixelToTileCoordinates(mMap, width, height); + + MapObject *object = new MapObject(name, type, pos, QSizeF(size.x(), + size.y())); + + if (gid) { + const Cell cell = cellForGid(gid); + object->setTile(cell.tile); + } + + while (xml.readNextStartElement()) { + if (xml.name() == "properties") { + object->mergeProperties(readProperties()); + } else if (xml.name() == "polygon") { + object->setPolygon(readPolygon()); + object->setShape(MapObject::Polygon); + } else if (xml.name() == "polyline") { + object->setPolygon(readPolygon()); + object->setShape(MapObject::Polyline); + } else { + readUnknownElement(); + } + } + + return object; +} + +QPolygonF MapReaderPrivate::readPolygon() +{ + Q_ASSERT(xml.isStartElement() && (xml.name() == "polygon" || + xml.name() == "polyline")); + + const QXmlStreamAttributes atts = xml.attributes(); + const QString points = atts.value(QLatin1String("points")).toString(); + const QStringList pointsList = points.split(QLatin1Char(' '), + QString::SkipEmptyParts); + + QPolygonF polygon; + bool ok = true; + + foreach (const QString &point, pointsList) { + const int commaPos = point.indexOf(QLatin1Char(',')); + if (commaPos == -1) { + ok = false; + break; + } + + const int x = point.left(commaPos).toInt(&ok); + if (!ok) + break; + const int y = point.mid(commaPos + 1).toInt(&ok); + if (!ok) + break; + + polygon.append(pixelToTileCoordinates(mMap, x, y)); + } + + if (!ok) + xml.raiseError(tr("Invalid points data for polygon")); + + xml.skipCurrentElement(); + return polygon; +} + +Properties MapReaderPrivate::readProperties() +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "properties"); + + Properties properties; + + while (xml.readNextStartElement()) { + if (xml.name() == "property") + readProperty(&properties); + else + readUnknownElement(); + } + + return properties; +} + +void MapReaderPrivate::readProperty(Properties *properties) +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "property"); + + const QXmlStreamAttributes atts = xml.attributes(); + QString propertyName = atts.value(QLatin1String("name")).toString(); + QString propertyValue = atts.value(QLatin1String("value")).toString(); + + while (xml.readNext() != QXmlStreamReader::Invalid) { + if (xml.isEndElement()) { + break; + } else if (xml.isCharacters() && !xml.isWhitespace()) { + if (propertyValue.isEmpty()) + propertyValue = xml.text().toString(); + } else if (xml.isStartElement()) { + readUnknownElement(); + } + } + + properties->insert(propertyName, propertyValue); +} + + +MapReader::MapReader() + : d(new MapReaderPrivate(this)) +{ +} + +MapReader::~MapReader() +{ + delete d; +} + +Map *MapReader::readMap(QIODevice *device, const QString &path) +{ + return d->readMap(device, path); +} + +Map *MapReader::readMap(const QString &fileName) +{ + QFile file(fileName); + if (!d->openFile(&file)) + return 0; + + return readMap(&file, QFileInfo(fileName).absolutePath()); +} + +Tileset *MapReader::readTileset(QIODevice *device, const QString &path) +{ + return d->readTileset(device, path); +} + +Tileset *MapReader::readTileset(const QString &fileName) +{ + QFile file(fileName); + if (!d->openFile(&file)) + return 0; + + Tileset *tileset = readTileset(&file, QFileInfo(fileName).absolutePath()); + if (tileset) + tileset->setFileName(fileName); + + return tileset; +} + +QString MapReader::errorString() const +{ + return d->errorString(); +} + +QString MapReader::resolveReference(const QString &reference, + const QString &mapPath) +{ + if (QDir::isRelativePath(reference)) + return mapPath + QLatin1Char('/') + reference; + else + return reference; +} + +QImage MapReader::readExternalImage(const QString &source) +{ + return QImage(source); +} + +Tileset *MapReader::readExternalTileset(const QString &source, + QString *error) +{ + MapReader reader; + Tileset *tileset = reader.readTileset(source); + if (!tileset) + *error = reader.errorString(); + return tileset; +} |
