/*
* Copyright 2004-2010, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
* Copyright 2004-2008, Adam Turk <aturk@biggeruniverse.com>
*
* This file is part of libtiled-java.
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or (at
* your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, see <http://www.gnu.org/licenses/>.
*/
package tiled.io;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.zip.GZIPOutputStream;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import tiled.core.*;
import tiled.core.Map;
import tiled.io.xml.XMLWriter;
import tiled.util.Base64;
import tiled.util.Converter;
import tiled.util.Util;
/**
* A writer for Tiled's TMX map format.
*/
public class TMXMapWriter
{
private static final int LAST_BYTE = 0x000000FF;
private static final boolean encodeLayerData = true;
private static final boolean compressLayerData = encodeLayerData;
/**
* Saves a map to an XML file.
*
* @param filename the filename of the map file
*/
public void writeMap(Map map, String filename) throws Exception {
OutputStream os = new FileOutputStream(filename);
if (filename.endsWith(".tmx.gz")) {
os = new GZIPOutputStream(os);
}
Writer writer = new OutputStreamWriter(os, Charset.forName("UTF-8"));
XMLWriter xmlWriter = new XMLWriter(writer);
xmlWriter.startDocument();
writeMap(map, xmlWriter, filename);
xmlWriter.endDocument();
writer.flush();
if (os instanceof GZIPOutputStream) {
((GZIPOutputStream)os).finish();
}
}
/**
* Saves a tileset to an XML file.
*
* @param filename the filename of the tileset file
*/
public void writeTileset(TileSet set, String filename) throws Exception {
OutputStream os = new FileOutputStream(filename);
Writer writer = new OutputStreamWriter(os, Charset.forName("UTF-8"));
XMLWriter xmlWriter = new XMLWriter(writer);
xmlWriter.startDocument();
writeTileset(set, xmlWriter, filename);
xmlWriter.endDocument();
writer.flush();
}
public void writeMap(Map map, OutputStream out, String basePath) throws Exception {
Writer writer = new OutputStreamWriter(out);
XMLWriter xmlWriter = new XMLWriter(writer);
xmlWriter.startDocument();
writeMap(map, xmlWriter, basePath);
xmlWriter.endDocument();
writer.flush();
}
public void writeMap(Map map, OutputStream out) throws Exception {
Writer writer = new OutputStreamWriter(out,Charset.forName("UTF-8"));
XMLWriter xmlWriter = new XMLWriter(writer);
xmlWriter.startDocument();
writeMap(map, xmlWriter, "/.");
xmlWriter.endDocument();
writer.flush();
}
public void writeTileset(TileSet set, OutputStream out) throws Exception {
Writer writer = new OutputStreamWriter(out, Charset.forName("UTF-8"));
XMLWriter xmlWriter = new XMLWriter(writer);
xmlWriter.startDocument();
writeTileset(set, xmlWriter, "/.");
xmlWriter.endDocument();
writer.flush();
}
private static void writeMap(Map map, XMLWriter w, String wp) throws IOException {
w.writeDocType("map", null, "http://mapeditor.org/dtd/1.0/map.dtd");
w.startElement("map");
w.writeAttribute("version", "1.0");
switch (map.getOrientation()) {
case Map.ORIENTATION_ORTHOGONAL:
w.writeAttribute("orientation", "orthogonal"); break;
case Map.ORIENTATION_ISOMETRIC:
w.writeAttribute("orientation", "isometric"); break;
case Map.ORIENTATION_HEXAGONAL:
w.writeAttribute("orientation", "hexagonal"); break;
case Map.ORIENTATION_SHIFTED:
w.writeAttribute("orientation", "shifted"); break;
}
w.writeAttribute("width", map.getWidth());
w.writeAttribute("height", map.getHeight());
w.writeAttribute("tilewidth", map.getTileWidth());
w.writeAttribute("tileheight", map.getTileHeight());
writeProperties(map.getProperties(), w);
int firstgid = 1;
for (TileSet tileset : map.getTilesets()) {
tileset.setFirstGid(firstgid);
writeTilesetReference(tileset, w, wp);
firstgid += tileset.getMaxTileId() + 1;
}
for (MapLayer layer : map) {
writeMapLayer(layer, w, wp);
}
w.endElement();
}
private static void writeProperties(Properties props, XMLWriter w) throws
IOException
{
if (!props.isEmpty()) {
final SortedSet<Object> propertyKeys = new TreeSet<Object>();
propertyKeys.addAll(props.keySet());
w.startElement("properties");
for (Object propertyKey : propertyKeys) {
final String key = (String) propertyKey;
final String property = props.getProperty(key);
w.startElement("property");
w.writeAttribute("name", key);
if (property.indexOf('\n') == -1) {
w.writeAttribute("value", property);
} else {
// Save multiline values as character data
w.writeCDATA(property);
}
w.endElement();
}
w.endElement();
}
}
/**
* Writes a reference to an external tileset into a XML document. In the
* case where the tileset is not stored in an external file, writes the
* contents of the tileset instead.
*
* @param set the tileset to write a reference to
* @param w the XML writer to write to
* @param wp the working directory of the map
* @throws java.io.IOException
*/
private static void writeTilesetReference(TileSet set, XMLWriter w, String wp)
throws IOException {
String source = set.getSource();
if (source == null) {
writeTileset(set, w, wp);
} else {
w.startElement("tileset");
w.writeAttribute("firstgid", set.getFirstGid());
w.writeAttribute("source", Util.getRelativePath(wp, source));
if (set.getBaseDir() != null) {
w.writeAttribute("basedir", set.getBaseDir());
}
w.endElement();
}
}
private static void writeTileset(TileSet set, XMLWriter w, String wp)
throws IOException {
String tileBitmapFile = set.getTilebmpFile();
String name = set.getName();
w.startElement("tileset");
w.writeAttribute("firstgid", set.getFirstGid());
if (name != null) {
w.writeAttribute("name", name);
}
if (tileBitmapFile != null) {
w.writeAttribute("tilewidth", set.getTileWidth());
w.writeAttribute("tileheight", set.getTileHeight());
final int tileSpacing = set.getTileSpacing();
final int tileMargin = set.getTileMargin();
if (tileSpacing != 0) {
w.writeAttribute("spacing", tileSpacing);
}
if (tileMargin != 0) {
w.writeAttribute("margin", tileMargin);
}
}
if (set.getBaseDir() != null) {
w.writeAttribute("basedir", set.getBaseDir());
}
if (tileBitmapFile != null) {
w.startElement("image");
w.writeAttribute("source", Util.getRelativePath(wp, tileBitmapFile));
RGB trans = set.getTransparentColor();
if (trans != null) {
w.writeAttribute("trans", Integer.toHexString(Converter.RGBtoInt(trans)));
}
w.endElement();
// Write tile properties when necessary.
for (Tile tile : set) {
// todo: move the null check back into the iterator?
if (tile != null && !tile.getProperties().isEmpty()) {
w.startElement("tile");
w.writeAttribute("id", tile.getId());
writeProperties(tile.getProperties(), w);
w.endElement();
}
}
} else {
// Check to see if there is a need to write tile elements
boolean needWrite = false;
// As long as one has properties, they all need to be written.
// TODO: This shouldn't be necessary
for (Tile tile : set) {
if (!tile.getProperties().isEmpty()) {
needWrite = true;
break;
}
}
if (needWrite) {
for (Tile tile : set) {
// todo: move this check back into the iterator?
if (tile != null) {
writeTile(tile, w);
}
}
}
}
w.endElement();
}
private static void writeObjectGroup(ObjectGroup o, XMLWriter w, String wp)
throws IOException
{
Iterator<MapObject> itr = o.getObjects();
while (itr.hasNext()) {
writeMapObject(itr.next(), w, wp);
}
}
/**
* Writes this layer to an XMLWriter. This should be done <b>after</b> the
* first global ids for the tilesets are determined, in order for the right
* gids to be written to the layer data.
*/
private static void writeMapLayer(MapLayer l, XMLWriter w, String wp) throws IOException {
Rectangle bounds = l.getBounds();
if (l instanceof ObjectGroup) {
w.startElement("objectgroup");
} else {
w.startElement("layer");
}
w.writeAttribute("name", l.getName());
w.writeAttribute("width", bounds.width);
w.writeAttribute("height", bounds.height);
if (bounds.x != 0) {
w.writeAttribute("x", bounds.x);
}
if (bounds.y != 0) {
w.writeAttribute("y", bounds.y);
}
if (!l.isVisible()) {
w.writeAttribute("visible", "0");
}
if (l.getOpacity() < 1.0f) {
w.writeAttribute("opacity", l.getOpacity());
}
writeProperties(l.getProperties(), w);
if (l instanceof ObjectGroup){
writeObjectGroup((ObjectGroup) l, w, wp);
} else if (l instanceof TileLayer) {
final TileLayer tl = (TileLayer) l;
w.startElement("data");
if (encodeLayerData) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStream out;
w.writeAttribute("encoding", "base64");
if (compressLayerData) {
w.writeAttribute("compression", "gzip");
out = new GZIPOutputStream(baos);
} else {
out = baos;
}
for (int y = 0; y < l.getHeight(); y++) {
for (int x = 0; x < l.getWidth(); x++) {
Tile tile = tl.getTileAt(x + bounds.x,
y + bounds.y);
int gid = 0;
if (tile != null) {
gid = tile.getGid();
}
out.write(gid & LAST_BYTE);
out.write(gid >> 8 & LAST_BYTE);
out.write(gid >> 16 & LAST_BYTE);
out.write(gid >> 24 & LAST_BYTE);
}
}
if (compressLayerData) {
((GZIPOutputStream)out).finish();
}
w.writeCDATA(new String(Base64.encode(baos.toByteArray())));
} else {
for (int y = 0; y < l.getHeight(); y++) {
for (int x = 0; x < l.getWidth(); x++) {
Tile tile = tl.getTileAt(x + bounds.x, y + bounds.y);
int gid = 0;
if (tile != null) {
gid = tile.getGid();
}
w.startElement("tile");
w.writeAttribute("gid", gid);
w.endElement();
}
}
}
w.endElement();
boolean tilePropertiesElementStarted = false;
for (int y = 0; y < l.getHeight(); y++) {
for (int x = 0; x < l.getWidth(); x++) {
Properties tip = tl.getTileInstancePropertiesAt(x, y);
if (tip != null && !tip.isEmpty()) {
if (!tilePropertiesElementStarted) {
w.startElement("tileproperties");
tilePropertiesElementStarted = true;
}
w.startElement("tile");
w.writeAttribute("x", x);
w.writeAttribute("y", y);
writeProperties(tip, w);
w.endElement();
}
}
}
if (tilePropertiesElementStarted)
w.endElement();
}
w.endElement();
}
/**
* Used to write tile elements for tilesets not based on a tileset image.
*
* @param tile the tile instance that should be written
* @param w the writer to write to
* @throws IOException when an io error occurs
*/
private static void writeTile(Tile tile, XMLWriter w) throws IOException {
w.startElement("tile");
w.writeAttribute("id", tile.getId());
writeProperties(tile.getProperties(), w);
if (tile instanceof AnimatedTile)
writeAnimation(((AnimatedTile)tile).getSprite(), w);
w.endElement();
}
private static void writeAnimation(Sprite s, XMLWriter w) throws IOException {
w.startElement("animation");
for (int k = 0; k < s.getTotalKeys(); k++) {
Sprite.KeyFrame key = s.getKey(k);
w.startElement("keyframe");
w.writeAttribute("name", key.getName());
for (int it = 0; it < key.getTotalFrames(); it++) {
Tile stile = key.getFrame(it);
w.startElement("tile");
w.writeAttribute("gid", stile.getGid());
w.endElement();
}
w.endElement();
}
w.endElement();
}
private static void writeMapObject(MapObject mapObject, XMLWriter w, String wp)
throws IOException
{
w.startElement("object");
w.writeAttribute("name", mapObject.getName());
if (mapObject.getType().length() != 0)
w.writeAttribute("type", mapObject.getType());
w.writeAttribute("x", mapObject.getX());
w.writeAttribute("y", mapObject.getY());
if (mapObject.getWidth() != 0)
w.writeAttribute("width", mapObject.getWidth());
if (mapObject.getHeight() != 0)
w.writeAttribute("height", mapObject.getHeight());
writeProperties(mapObject.getProperties(), w);
if (mapObject.getImageSource().length() > 0) {
w.startElement("image");
w.writeAttribute("source",
Util.getRelativePath(wp, mapObject.getImageSource()));
w.endElement();
}
w.endElement();
}
public boolean accept(File pathName) {
try {
String path = pathName.getCanonicalPath();
if (path.endsWith(".tmx") || path.endsWith(".tsx") || path.endsWith(".tmx.gz")) {
return true;
}
} catch (IOException e) {}
return false;
}
}