1 /*-
2  * #%L
3  * This file is part of libtiled-java.
4  * %%
5  * Copyright (C) 2004 - 2020 Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
6  * Copyright (C) 2004 - 2020 Adam Turk <aturk@biggeruniverse.com>
7  * Copyright (C) 2016 - 2020 Mike Thomas <mikepthomas@outlook.com>
8  * %%
9  * Redistribution and use in source and binary forms, with or without
10  * modification, are permitted provided that the following conditions are met:
11  *
12  * 1. Redistributions of source code must retain the above copyright notice,
13  *    this list of conditions and the following disclaimer.
14  * 2. Redistributions in binary form must reproduce the above copyright notice,
15  *    this list of conditions and the following disclaimer in the documentation
16  *    and/or other materials provided with the distribution.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28  * POSSIBILITY OF SUCH DAMAGE.
29  * #L%
30  */
31 package org.mapeditor.io;
32 
33 import java.awt.Color;
34 import java.awt.Rectangle;
35 import java.io.ByteArrayOutputStream;
36 import java.io.File;
37 import java.io.FileOutputStream;
38 import java.io.IOException;
39 import java.io.OutputStream;
40 import java.io.OutputStreamWriter;
41 import java.io.Writer;
42 import java.nio.charset.Charset;
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.Iterator;
46 import java.util.List;
47 import java.util.Set;
48 import java.util.TreeSet;
49 import java.util.zip.DeflaterOutputStream;
50 import java.util.zip.GZIPOutputStream;
51 
52 import javax.xml.bind.DatatypeConverter;
53 
54 import org.mapeditor.core.AnimatedTile;
55 import org.mapeditor.core.MapLayer;
56 import org.mapeditor.core.Map;
57 import org.mapeditor.core.MapObject;
58 import org.mapeditor.core.ObjectGroup;
59 import org.mapeditor.core.Group;
60 import org.mapeditor.core.Orientation;
61 import org.mapeditor.core.Properties;
62 import org.mapeditor.core.Sprite;
63 import org.mapeditor.core.Tile;
64 import org.mapeditor.core.TileLayer;
65 import org.mapeditor.core.TileSet;
66 import org.mapeditor.io.xml.XMLWriter;
67 
68 /**
69  * A writer for Tiled's TMX map format.
70  *
71  * @version 1.4.2
72  */
73 public class TMXMapWriter {
74 
75     private static final int LAST_BYTE = 0x000000FF;
76 
77     private static final boolean ENCODE_LAYER_DATA = true;
78     private static final boolean COMPRESS_LAYER_DATA = ENCODE_LAYER_DATA;
79 
80     private HashMap<TileSet, Integer> firstGidPerTileset;
81 
82     public static class Settings {
83 
84         @Deprecated
85         public static final String LAYER_COMPRESSION_METHOD_GZIP = "gzip";
86         public static final String LAYER_COMPRESSION_METHOD_ZLIB = "zlib";
87 
88         public String layerCompressionMethod = LAYER_COMPRESSION_METHOD_ZLIB;
89     }
90     public Settings settings = new Settings();
91 
92     /**
93      * Saves a map to an XML file.
94      *
95      * @param map a {@link org.mapeditor.core.Map} object.
96      * @param filename the filename of the map file
97      * @throws java.io.IOException if any.
98      */
writeMap(Map map, String filename)99     public void writeMap(Map map, String filename) throws IOException {
100         OutputStream os = new FileOutputStream(filename);
101 
102         if (filename.endsWith(".tmx.gz")) {
103             os = new GZIPOutputStream(os);
104         }
105 
106         Writer writer = new OutputStreamWriter(os, Charset.forName("UTF-8"));
107         XMLWriter xmlWriter = new XMLWriter(writer);
108 
109         xmlWriter.startDocument();
110         writeMap(map, xmlWriter, filename);
111         xmlWriter.endDocument();
112 
113         writer.flush();
114 
115         if (os instanceof GZIPOutputStream) {
116             ((GZIPOutputStream) os).finish();
117         }
118     }
119 
120     /**
121      * Saves a tileset to an XML file.
122      *
123      * @param set a {@link org.mapeditor.core.TileSet} object.
124      * @param filename the filename of the tileset file
125      * @throws java.io.IOException if any.
126      */
writeTileset(TileSet set, String filename)127     public void writeTileset(TileSet set, String filename) throws IOException {
128         OutputStream os = new FileOutputStream(filename);
129         Writer writer = new OutputStreamWriter(os, Charset.forName("UTF-8"));
130         XMLWriter xmlWriter = new XMLWriter(writer);
131 
132         xmlWriter.startDocument();
133         writeTileset(set, xmlWriter, filename);
134         xmlWriter.endDocument();
135 
136         writer.flush();
137     }
138 
139     /**
140      * writeMap.
141      *
142      * @param map a {@link org.mapeditor.core.Map} object.
143      * @param out a {@link java.io.OutputStream} object.
144      * @throws java.lang.Exception if any.
145      */
writeMap(Map map, OutputStream out)146     public void writeMap(Map map, OutputStream out) throws Exception {
147         Writer writer = new OutputStreamWriter(out, Charset.forName("UTF-8"));
148         XMLWriter xmlWriter = new XMLWriter(writer);
149 
150         xmlWriter.startDocument();
151         writeMap(map, xmlWriter, "/.");
152         xmlWriter.endDocument();
153 
154         writer.flush();
155     }
156 
157     /**
158      * writeTileset.
159      *
160      * @param set a {@link org.mapeditor.core.TileSet} object.
161      * @param out a {@link java.io.OutputStream} object.
162      * @throws java.lang.Exception if any.
163      */
writeTileset(TileSet set, OutputStream out)164     public void writeTileset(TileSet set, OutputStream out) throws Exception {
165         Writer writer = new OutputStreamWriter(out, Charset.forName("UTF-8"));
166         XMLWriter xmlWriter = new XMLWriter(writer);
167 
168         xmlWriter.startDocument();
169         writeTileset(set, xmlWriter, "/.");
170         xmlWriter.endDocument();
171 
172         writer.flush();
173     }
174 
writeMap(Map map, XMLWriter w, String wp)175     private void writeMap(Map map, XMLWriter w, String wp) throws IOException {
176 //        w.writeDocType("map", null, "http://mapeditor.org/dtd/1.0/map.dtd");
177         w.startElement("map");
178 
179         w.writeAttribute("version", "1.2");
180 
181         if (!map.getTiledversion().isEmpty()) {
182             w.writeAttribute("tiledversion", map.getTiledversion());
183         }
184 
185         Orientation orientation = map.getOrientation();
186         w.writeAttribute("orientation", orientation.value());
187         w.writeAttribute("renderorder", map.getRenderorder().value());
188         w.writeAttribute("width", map.getWidth());
189         w.writeAttribute("height", map.getHeight());
190         w.writeAttribute("tilewidth", map.getTileWidth());
191         w.writeAttribute("tileheight", map.getTileHeight());
192         w.writeAttribute("infinite", map.getInfinite());
193 
194         w.writeAttribute("nextlayerid", map.getNextlayerid());
195         w.writeAttribute("nextobjectid", map.getNextobjectid());
196 
197         switch (orientation) {
198             case HEXAGONAL:
199                 w.writeAttribute("hexsidelength", map.getHexSideLength());
200             case STAGGERED:
201                 w.writeAttribute("staggeraxis", map.getStaggerAxis().value());
202                 w.writeAttribute("staggerindex", map.getStaggerIndex().value());
203         }
204 
205         writeProperties(map.getProperties(), w);
206 
207         firstGidPerTileset = new HashMap<>();
208         int firstgid = 1;
209         for (TileSet tileset : map.getTileSets()) {
210             setFirstGidForTileset(tileset, firstgid);
211             writeTilesetReference(tileset, w, wp);
212             firstgid += tileset.getMaxTileId() + 1;
213         }
214 
215         for (MapLayer layer : map.getLayers()) {
216             if (layer instanceof TileLayer) {
217                 writeMapLayer((TileLayer) layer, w, wp);
218             } else if (layer instanceof ObjectGroup) {
219                 writeObjectGroup((ObjectGroup) layer, w, wp);
220             } else if (layer instanceof Group) {
221                 writeGroup((Group) layer, w, wp);
222             }
223         }
224         firstGidPerTileset = null;
225 
226         w.endElement();
227     }
228 
writeGroup(Group group, XMLWriter w, String wp)229     private void writeGroup(Group group, XMLWriter w, String wp) throws IOException {
230         w.startElement("group");
231 
232         writeLayerAttributes(group, w);
233         writeProperties(group.getProperties(), w);
234 
235         for (MapLayer layer : group.getLayers()) {
236             if (layer instanceof TileLayer) {
237                 writeMapLayer((TileLayer) layer, w, wp);
238             } else if (layer instanceof ObjectGroup) {
239                 writeObjectGroup((ObjectGroup) layer, w, wp);
240             } else if (layer instanceof Group) {
241                 writeGroup((Group) layer, w, wp);
242             } // TODO: Image Layer writing
243         }
244 
245         w.endElement();
246     }
247 
writeProperties(Properties props, XMLWriter w)248     private void writeProperties(Properties props, XMLWriter w) throws
249             IOException {
250         if (props != null && !props.isEmpty()) {
251             final Set<Object> propertyKeys = new TreeSet<>();
252             propertyKeys.addAll(props.keySet());
253             w.startElement("properties");
254             for (Object propertyKey : propertyKeys) {
255                 final String key = (String) propertyKey;
256                 final String property = props.getProperty(key);
257                 w.startElement("property");
258                 w.writeAttribute("name", key);
259                 if (property.indexOf('\n') == -1) {
260                     if ("true".equals(property) || "false".equals(property)) {
261                         w.writeAttribute("type", "bool");
262                     }
263                     w.writeAttribute("value", property);
264                 } else {
265                     // Save multiline values as character data
266                     w.writeCDATA(property);
267                 }
268                 w.endElement();
269             }
270             w.endElement();
271         }
272     }
273 
274     /**
275      * Writes a reference to an external tileset into a XML document. In the
276      * case where the tileset is not stored in an external file, writes the
277      * contents of the tileset instead.
278      *
279      * @param set the tileset to write a reference to
280      * @param w the XML writer to write to
281      * @param wp the working directory of the map
282      * @throws java.io.IOException
283      */
writeTilesetReference(TileSet set, XMLWriter w, String wp)284     private void writeTilesetReference(TileSet set, XMLWriter w, String wp)
285             throws IOException {
286 
287         String source = set.getSource();
288 
289         if (source == null) {
290             writeTileset(set, w, wp);
291         } else {
292             w.startElement("tileset");
293             w.writeAttribute("firstgid", getFirstGidForTileset(set));
294             w.writeAttribute("source", getRelativePath(wp, source));
295             w.endElement();
296         }
297     }
298 
writeTileset(TileSet set, XMLWriter w, String wp)299     private void writeTileset(TileSet set, XMLWriter w, String wp)
300             throws IOException {
301 
302         String tileBitmapFile = set.getTilebmpFile();
303         String name = set.getName();
304 
305         w.startElement("tileset");
306         w.writeAttribute("firstgid", getFirstGidForTileset(set));
307 
308         if (name != null) {
309             w.writeAttribute("name", name);
310         }
311 
312         if (tileBitmapFile != null) {
313             w.writeAttribute("tilewidth", set.getTileWidth());
314             w.writeAttribute("tileheight", set.getTileHeight());
315 
316             final int tileSpacing = set.getTileSpacing();
317             final int tileMargin = set.getTileMargin();
318             if (tileSpacing != 0) {
319                 w.writeAttribute("spacing", tileSpacing);
320             }
321             if (tileMargin != 0) {
322                 w.writeAttribute("margin", tileMargin);
323             }
324         }
325 
326         if (tileBitmapFile != null) {
327             w.startElement("image");
328             w.writeAttribute("source", getRelativePath(wp, tileBitmapFile));
329 
330             Color trans = set.getTransparentColor();
331             if (trans != null) {
332                 w.writeAttribute("trans", Integer.toHexString(
333                         trans.getRGB()).substring(2));
334             }
335             w.endElement();
336 
337             // Write tile properties when necessary.
338             for (Tile tile : set) {
339                 // todo: move the null check back into the iterator?
340                 if (tile != null
341                         && (!tile.getProperties().isEmpty()
342                         || !tile.getType().isEmpty())) {
343                     w.startElement("tile");
344                     w.writeAttribute("id", tile.getId());
345                     if (!tile.getType().isEmpty()) {
346                         w.writeAttribute("type", tile.getType());
347                     }
348                     if (!tile.getProperties().isEmpty()) {
349                         writeProperties(tile.getProperties(), w);
350                     }
351                     w.endElement();
352                 }
353             }
354         } else {
355             // Check to see if there is a need to write tile elements
356             boolean needWrite = false;
357 
358             // As long as one has properties, they all need to be written.
359             // TODO: This shouldn't be necessary
360             for (Tile tile : set) {
361                 if (!tile.getProperties().isEmpty()
362                         || !tile.getType().isEmpty()
363                         || tile.getSource() != null) {
364                     needWrite = true;
365                     break;
366                 }
367             }
368 
369             if (needWrite) {
370                 w.writeAttribute("tilewidth", set.getTileWidth());
371                 w.writeAttribute("tileheight", set.getTileHeight());
372                 w.writeAttribute("tilecount", set.size());
373                 w.writeAttribute("columns", set.getColumns());
374 
375                 for (Tile tile : set) {
376                     // todo: move this check back into the iterator?
377                     if (tile != null) {
378                         writeTile(tile, w, wp);
379                     }
380                 }
381             }
382         }
383         w.endElement();
384     }
385 
writeObjectGroup(ObjectGroup o, XMLWriter w, String wp)386     private void writeObjectGroup(ObjectGroup o, XMLWriter w, String wp)
387             throws IOException {
388         w.startElement("objectgroup");
389 
390         if (o.getColor() != null && o.getColor().isEmpty()) {
391             w.writeAttribute("color", o.getColor());
392         }
393         if (o.getDraworder() != null && !o.getDraworder().equalsIgnoreCase("topdown")) {
394             w.writeAttribute("draworder", o.getDraworder());
395         }
396         writeLayerAttributes(o, w);
397         writeProperties(o.getProperties(), w);
398 
399         Iterator<MapObject> itr = o.getObjects().iterator();
400         while (itr.hasNext()) {
401             writeMapObject(itr.next(), w, wp);
402         }
403 
404         w.endElement();
405     }
406 
407     /**
408      * Writes all the standard layer attributes to the XML writer.
409      * @param l the map layer to write attributes
410      * @param w the {@code XMLWriter} instance to write to.
411      * @throws IOException if an error occurs while writing.
412      */
writeLayerAttributes(MapLayer l, XMLWriter w)413     private void writeLayerAttributes(MapLayer l, XMLWriter w) throws IOException {
414         Rectangle bounds = l.getBounds();
415 
416         w.writeAttribute("id", l.getId());
417 
418         w.writeAttribute("name", l.getName());
419         if (l instanceof TileLayer) {
420             if (bounds.width != 0) {
421                 w.writeAttribute("width", bounds.width);
422             }
423             if (bounds.height != 0) {
424                 w.writeAttribute("height", bounds.height);
425             }
426         }
427         if (bounds.x != 0) {
428             w.writeAttribute("x", bounds.x);
429         }
430         if (bounds.y != 0) {
431             w.writeAttribute("y", bounds.y);
432         }
433 
434         Boolean isVisible = l.isVisible();
435         if (isVisible != null && !isVisible) {
436             w.writeAttribute("visible", "0");
437         }
438         Float opacity = l.getOpacity();
439         if (opacity != null && opacity < 1.0f) {
440             w.writeAttribute("opacity", opacity);
441         }
442 
443         if (l.getOffsetX() != null && l.getOffsetX() != 0) {
444             w.writeAttribute("offsetx", l.getOffsetX());
445         }
446         if (l.getOffsetY() != null && l.getOffsetY() != 0) {
447             w.writeAttribute("offsety", l.getOffsetY());
448         }
449 
450         if (l.getLocked() != null && l.getLocked() != 0) {
451             w.writeAttribute("locked", l.getLocked());
452         }
453     }
454 
455     /**
456      * Writes this layer to an XMLWriter. This should be done <b>after</b> the
457      * first global ids for the tilesets are determined, in order for the right
458      * gids to be written to the layer data.
459      */
writeMapLayer(TileLayer l, XMLWriter w, String wp)460     private void writeMapLayer(TileLayer l, XMLWriter w, String wp) throws IOException {
461         Rectangle bounds = l.getBounds();
462 
463         w.startElement("layer");
464 
465         writeLayerAttributes(l, w);
466         writeProperties(l.getProperties(), w);
467 
468         final TileLayer tl = l;
469         w.startElement("data");
470         if (ENCODE_LAYER_DATA) {
471             ByteArrayOutputStream baos = new ByteArrayOutputStream();
472             OutputStream out;
473 
474             w.writeAttribute("encoding", "base64");
475 
476             DeflaterOutputStream dos;
477             if (COMPRESS_LAYER_DATA) {
478                 if (Settings.LAYER_COMPRESSION_METHOD_ZLIB.equalsIgnoreCase(settings.layerCompressionMethod)) {
479                     dos = new DeflaterOutputStream(baos);
480                 } else if (Settings.LAYER_COMPRESSION_METHOD_GZIP.equalsIgnoreCase(settings.layerCompressionMethod)) {
481                     dos = new GZIPOutputStream(baos);
482                 } else {
483                     throw new IOException("Unrecognized compression method \"" + settings.layerCompressionMethod + "\" for map layer " + l.getName());
484                 }
485                 out = dos;
486                 w.writeAttribute("compression", settings.layerCompressionMethod);
487             } else {
488                 out = baos;
489             }
490 
491             for (int y = 0; y < l.getHeight(); y++) {
492                 for (int x = 0; x < l.getWidth(); x++) {
493                     Tile tile = tl.getTileAt(x + bounds.x,
494                             y + bounds.y);
495                     int gid = 0;
496 
497                     if (tile != null) {
498                         gid = getGid(tile);
499                         gid |= tl.getFlagsAt(x, y);
500                     }
501 
502                     out.write(gid & LAST_BYTE);
503                     out.write(gid >> Byte.SIZE & LAST_BYTE);
504                     out.write(gid >> Byte.SIZE * 2 & LAST_BYTE);
505                     out.write(gid >> Byte.SIZE * 3 & LAST_BYTE);
506                 }
507             }
508 
509             if (COMPRESS_LAYER_DATA && dos != null) {
510                 dos.finish();
511             }
512 
513             byte[] dec = baos.toByteArray();
514             w.writeCDATA(DatatypeConverter.printBase64Binary(dec));
515         } else {
516             for (int y = 0; y < l.getHeight(); y++) {
517                 for (int x = 0; x < l.getWidth(); x++) {
518                     Tile tile = tl.getTileAt(x + bounds.x, y + bounds.y);
519                     int gid = 0;
520 
521                     if (tile != null) {
522                         gid = getGid(tile);
523                     }
524 
525                     w.startElement("tile");
526                     w.writeAttribute("gid", gid);
527                     w.endElement();
528                 }
529             }
530         }
531         w.endElement();
532 
533         boolean tilePropertiesElementStarted = false;
534 
535         for (int y = 0; y < l.getHeight(); y++) {
536             for (int x = 0; x < l.getWidth(); x++) {
537                 Properties tip = tl.getTileInstancePropertiesAt(x, y);
538 
539                 if (tip != null && !tip.isEmpty()) {
540                     if (!tilePropertiesElementStarted) {
541                         w.startElement("tileproperties");
542                         tilePropertiesElementStarted = true;
543                     }
544                     w.startElement("tile");
545 
546                     w.writeAttribute("x", x);
547                     w.writeAttribute("y", y);
548 
549                     writeProperties(tip, w);
550 
551                     w.endElement();
552                 }
553             }
554         }
555 
556         if (tilePropertiesElementStarted) {
557             w.endElement();
558         }
559 
560         w.endElement();
561     }
562 
563     /**
564      * Used to write tile elements for tilesets not based on a tileset image.
565      *
566      * @param tile the tile instance that should be written
567      * @param w the writer to write to
568      * @throws IOException when an io error occurs
569      */
writeTile(Tile tile, XMLWriter w, String wp)570     private void writeTile(Tile tile, XMLWriter w, String wp) throws IOException {
571         w.startElement("tile");
572         w.writeAttribute("id", tile.getId());
573 
574         if (!tile.getType().isEmpty()) {
575             w.writeAttribute("type", tile.getType());
576         }
577 
578         if (!tile.getProperties().isEmpty()) {
579             writeProperties(tile.getProperties(), w);
580         }
581 
582         if (tile.getSource() != null) {
583             writeImage(tile, w, wp);
584         }
585 
586         if (tile instanceof AnimatedTile) {
587             writeAnimation(((AnimatedTile) tile).getSprite(), w);
588         }
589 
590         w.endElement();
591     }
592 
writeImage(Tile t, XMLWriter w, String wp)593     private void writeImage(Tile t, XMLWriter w, String wp) throws IOException {
594         w.startElement("image");
595         w.writeAttribute("width", t.getWidth());
596         w.writeAttribute("height", t.getHeight());
597         w.writeAttribute("source", getRelativePath(wp, t.getSource()));
598         w.endElement();
599     }
600 
writeAnimation(Sprite s, XMLWriter w)601     private void writeAnimation(Sprite s, XMLWriter w) throws IOException {
602         w.startElement("animation");
603         for (int k = 0; k < s.getTotalKeys(); k++) {
604             Sprite.KeyFrame key = s.getKey(k);
605             w.startElement("keyframe");
606             w.writeAttribute("name", key.getName());
607             for (int it = 0; it < key.getTotalFrames(); it++) {
608                 Tile stile = key.getFrame(it);
609                 w.startElement("tile");
610                 w.writeAttribute("gid", getGid(stile));
611                 w.endElement();
612             }
613             w.endElement();
614         }
615         w.endElement();
616     }
617 
writeMapObject(MapObject mapObject, XMLWriter w, String wp)618     private void writeMapObject(MapObject mapObject, XMLWriter w, String wp)
619             throws IOException {
620         w.startElement("object");
621         w.writeAttribute("id", mapObject.getId());
622 
623         long gid = 0;
624         if (mapObject.getTile() != null) {
625             Tile t = mapObject.getTile();
626             gid = firstGidPerTileset.get(t.getTileSet()) + t.getId();
627         } else if (mapObject.getGid() != null) {
628             gid = mapObject.getGid();
629         }
630 
631         if (mapObject.getFlipHorizontal()) {
632             gid |= TMXMapReader.FLIPPED_HORIZONTALLY_FLAG;
633         }
634 
635         if (mapObject.getFlipVertical()) {
636             gid |= TMXMapReader.FLIPPED_VERTICALLY_FLAG;
637         }
638 
639         if (mapObject.getFlipDiagonal()) {
640             gid |= TMXMapReader.FLIPPED_DIAGONALLY_FLAG;
641         }
642 
643         if (gid != 0) {
644             w.writeAttribute("gid", gid);
645         }
646 
647         if (!mapObject.getName().isEmpty()) {
648             w.writeAttribute("name", mapObject.getName());
649         }
650 
651         if (mapObject.getType().length() != 0) {
652             w.writeAttribute("type", mapObject.getType());
653         }
654 
655         w.writeAttribute("x", mapObject.getX());
656         w.writeAttribute("y", mapObject.getY());
657 
658         // TODO: Implement Polygon, Ellipse & Polyline too
659         boolean isPoint = mapObject.getPoint() != null;
660         if (isPoint) {
661             w.startElement("point");
662             w.endElement();
663         }
664         else {
665             if (mapObject.getWidth() != 0) {
666                 w.writeAttribute("width", mapObject.getWidth());
667             }
668             if (mapObject.getHeight() != 0) {
669                 w.writeAttribute("height", mapObject.getHeight());
670             }
671         }
672 
673         if (mapObject.getRotation() != 0) {
674             w.writeAttribute("rotation", mapObject.getRotation());
675         }
676 
677         writeProperties(mapObject.getProperties(), w);
678 
679         if (mapObject.getImageSource().length() > 0) {
680             w.startElement("image");
681             w.writeAttribute("source",
682                     getRelativePath(wp, mapObject.getImageSource()));
683             w.endElement();
684         }
685 
686         w.endElement();
687     }
688 
689     /**
690      * Returns the relative path from one file to the other. The function
691      * expects absolute paths, relative paths will be converted to absolute
692      * using the working directory.
693      *
694      * @param from the path of the origin file
695      * @param to the path of the destination file
696      * @return the relative path from origin to destination
697      */
getRelativePath(String from, String to)698     public static String getRelativePath(String from, String to) {
699         if (!(new File(to)).isAbsolute()) {
700             return to;
701         }
702 
703         // Make the two paths absolute and unique
704         try {
705             from = new File(from).getCanonicalPath();
706             to = new File(to).getCanonicalPath();
707         } catch (IOException e) {
708             // todo: log this
709         }
710 
711         File fromFile = new File(from);
712         File toFile = new File(to);
713         List<String> fromParents = new ArrayList<>();
714         List<String> toParents = new ArrayList<>();
715 
716         // Iterate to find both parent lists
717         while (fromFile != null) {
718             fromParents.add(0, fromFile.getName());
719             fromFile = fromFile.getParentFile();
720         }
721         while (toFile != null) {
722             toParents.add(0, toFile.getName());
723             toFile = toFile.getParentFile();
724         }
725 
726         // Iterate while parents are the same
727         int shared = 0;
728         int maxShared = Math.min(fromParents.size(), toParents.size());
729         for (shared = 0; shared < maxShared; shared++) {
730             String fromParent = fromParents.get(shared);
731             String toParent = toParents.get(shared);
732             if (!fromParent.equals(toParent)) {
733                 break;
734             }
735         }
736 
737         // Append .. for each remaining parent in fromParents
738         StringBuilder relPathBuf = new StringBuilder();
739         for (int i = shared; i < fromParents.size() - 1; i++) {
740             relPathBuf.append("..").append(File.separator);
741         }
742 
743         // Add the remaining part in toParents
744         for (int i = shared; i < toParents.size() - 1; i++) {
745             relPathBuf.append(toParents.get(i)).append(File.separator);
746         }
747         relPathBuf.append(new File(to).getName());
748         String relPath = relPathBuf.toString();
749 
750         // Turn around the slashes when path is relative
751         try {
752             String absPath = new File(relPath).getCanonicalPath();
753 
754             if (!absPath.equals(relPath)) {
755                 // Path is not absolute, turn slashes around
756                 // Assumes: \ does not occur in file names
757                 relPath = relPath.replace('\\', '/');
758             }
759         } catch (IOException e) {
760         }
761 
762         return relPath;
763     }
764 
765     /**
766      * accept.
767      *
768      * @param pathName a {@link java.io.File} object.
769      * @return a boolean.
770      */
accept(File pathName)771     public boolean accept(File pathName) {
772         try {
773             String path = pathName.getCanonicalPath();
774             if (path.endsWith(".tmx") || path.endsWith(".tsx") || path.endsWith(".tmx.gz")) {
775                 return true;
776             }
777         } catch (IOException e) {
778         }
779         return false;
780     }
781 
782     /**
783      * Returns the global tile id of the given tile.
784      *
785      * @return global tile id of the given tile
786      */
getGid(Tile tile)787     private int getGid(Tile tile) {
788         TileSet tileset = tile.getTileSet();
789         if (tileset != null) {
790             return tile.getId() + getFirstGidForTileset(tileset);
791         }
792         return tile.getId();
793     }
794 
setFirstGidForTileset(TileSet tileset, int firstGid)795     private void setFirstGidForTileset(TileSet tileset, int firstGid) {
796         firstGidPerTileset.put(tileset, firstGid);
797     }
798 
getFirstGidForTileset(TileSet tileset)799     private int getFirstGidForTileset(TileSet tileset) {
800         if (firstGidPerTileset == null) {
801             return 1;
802         }
803         return firstGidPerTileset.get(tileset);
804     }
805 }
806