/*
* Copyright 2004-2010, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
* Copyright 2004-2008, Adam Turk <aturk@biggeruniverse.com>
*
* This file is part of libtiled-java.
*
* 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.
*/
package tiled.io;
import java.awt.Color;
import java.awt.Rectangle;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
import tiled.core.*;
import tiled.core.Map;
import tiled.io.xml.XMLWriter;
import tiled.util.Base64;
/**
* 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;
private HashMap<String, Integer> firstGidPerTileset;
public static class Settings {
public static final String LAYER_COMPRESSION_METHOD_GZIP = "gzip";
public static final String LAYER_COMPRESSION_METHOD_ZLIB = "zlib";
public String layerCompressionMethod = LAYER_COMPRESSION_METHOD_GZIP;
}
public Settings settings = new Settings();
/**
* 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) 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 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);
firstGidPerTileset = new HashMap<String, Integer>();
int firstgid = 1;
for (TileSet tileset : map.getTileSets()) {
setFirstGidForTileset(tileset, firstgid);
writeTilesetReference(tileset, w, wp);
firstgid += tileset.getMaxTileId() + 1;
}
for (MapLayer layer : map) {
writeMapLayer(layer, w, wp);
}
firstGidPerTileset = null;
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 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", getFirstGidForTileset(set));
w.writeAttribute("source", getRelativePath(wp, source));
if (set.getBaseDir() != null) {
w.writeAttribute("basedir", set.getBaseDir());
}
w.endElement();
}
}
private void writeTileset(TileSet set, XMLWriter w, String wp)
throws IOException {
String tileBitmapFile = set.getTilebmpFile();
String name = set.getName();
w.startElement("tileset");
w.writeAttribute("firstgid", getFirstGidForTileset(set));
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", getRelativePath(wp, tileBitmapFile));
Color trans = set.getTransparentColor();
if (trans != null) {
w.writeAttribute("trans", Integer.toHexString(
trans.getRGB()).substring(2));
}
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 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());
if (bounds.width != 0) {
w.writeAttribute("width", bounds.width);
}
if (bounds.height != 0) {
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");
DeflaterOutputStream dos;
if (compressLayerData) {
if (Settings.LAYER_COMPRESSION_METHOD_ZLIB.equalsIgnoreCase(settings.layerCompressionMethod)) {
dos = new DeflaterOutputStream(baos);
} else if (Settings.LAYER_COMPRESSION_METHOD_GZIP.equalsIgnoreCase(settings.layerCompressionMethod)) {
dos = new GZIPOutputStream(baos);
} else {
throw new IOException("Unrecognized compression method \"" + settings.layerCompressionMethod + "\" for map layer " + l.getName());
}
out = dos;
w.writeAttribute("compression", settings.layerCompressionMethod);
} 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 = getGid(tile);
}
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 && dos != null) {
dos.finish();
}
w.writeCDATA(Base64.encodeToString(baos.toByteArray(), true));
} 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 = getGid(tile);
}
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 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 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", getGid(stile));
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",
getRelativePath(wp, mapObject.getImageSource()));
w.endElement();
}
w.endElement();
}
/**
* Returns the relative path from one file to the other. The function
* expects absolute paths, relative paths will be converted to absolute
* using the working directory.
*
* @param from the path of the origin file
* @param to the path of the destination file
* @return the relative path from origin to destination
*/
public static String getRelativePath(String from, String to) {
if(!(new File(to)).isAbsolute())
return to;
// Make the two paths absolute and unique
try {
from = new File(from).getCanonicalPath();
to = new File(to).getCanonicalPath();
} catch (IOException e) {
}
File fromFile = new File(from);
File toFile = new File(to);
Vector<String> fromParents = new Vector<String>();
Vector<String> toParents = new Vector<String>();
// Iterate to find both parent lists
while (fromFile != null) {
fromParents.add(0, fromFile.getName());
fromFile = fromFile.getParentFile();
}
while (toFile != null) {
toParents.add(0, toFile.getName());
toFile = toFile.getParentFile();
}
// Iterate while parents are the same
int shared = 0;
int maxShared = Math.min(fromParents.size(), toParents.size());
for (shared = 0; shared < maxShared; shared++) {
String fromParent = fromParents.get(shared);
String toParent = toParents.get(shared);
if (!fromParent.equals(toParent)) {
break;
}
}
// Append .. for each remaining parent in fromParents
StringBuffer relPathBuf = new StringBuffer();
for (int i = shared; i < fromParents.size() - 1; i++) {
relPathBuf.append(".." + File.separator);
}
// Add the remaining part in toParents
for (int i = shared; i < toParents.size() - 1; i++) {
relPathBuf.append(toParents.get(i) + File.separator);
}
relPathBuf.append(new File(to).getName());
String relPath = relPathBuf.toString();
// Turn around the slashes when path is relative
try {
String absPath = new File(relPath).getCanonicalPath();
if (!absPath.equals(relPath)) {
// Path is not absolute, turn slashes around
// Assumes: \ does not occur in file names
relPath = relPath.replace('\\', '/');
}
} catch (IOException e) {
}
return relPath;
}
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;
}
/**
* Returns the global tile id of the given tile.
*
* @return global tile id of the given tile
*/
private int getGid(Tile tile) {
TileSet tileset = tile.getTileSet();
if (tileset != null) {
return tile.getId() + getFirstGidForTileset(tileset);
}
return tile.getId();
}
private void setFirstGidForTileset(TileSet tileset, int firstGid) {
firstGidPerTileset.put(tileset.getName(), firstGid);
}
private int getFirstGidForTileset(TileSet tileset) {
return firstGidPerTileset.get(tileset.getName());
}
}