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