1 2 package com.badlogic.gdx.maps.tiled; 3 4 import java.io.BufferedInputStream; 5 import java.io.ByteArrayInputStream; 6 import java.io.IOException; 7 import java.io.InputStream; 8 import java.util.StringTokenizer; 9 import java.util.zip.GZIPInputStream; 10 import java.util.zip.InflaterInputStream; 11 12 import com.badlogic.gdx.assets.AssetLoaderParameters; 13 import com.badlogic.gdx.assets.loaders.AsynchronousAssetLoader; 14 import com.badlogic.gdx.assets.loaders.FileHandleResolver; 15 import com.badlogic.gdx.files.FileHandle; 16 import com.badlogic.gdx.graphics.Texture.TextureFilter; 17 import com.badlogic.gdx.graphics.g2d.TextureRegion; 18 import com.badlogic.gdx.maps.ImageResolver; 19 import com.badlogic.gdx.maps.MapLayer; 20 import com.badlogic.gdx.maps.MapObject; 21 import com.badlogic.gdx.maps.MapProperties; 22 import com.badlogic.gdx.maps.objects.EllipseMapObject; 23 import com.badlogic.gdx.maps.objects.PolygonMapObject; 24 import com.badlogic.gdx.maps.objects.PolylineMapObject; 25 import com.badlogic.gdx.maps.objects.RectangleMapObject; 26 import com.badlogic.gdx.maps.tiled.TiledMapTileLayer.Cell; 27 import com.badlogic.gdx.maps.tiled.objects.TiledMapTileMapObject; 28 import com.badlogic.gdx.math.Polygon; 29 import com.badlogic.gdx.math.Polyline; 30 import com.badlogic.gdx.utils.Base64Coder; 31 import com.badlogic.gdx.utils.GdxRuntimeException; 32 import com.badlogic.gdx.utils.StreamUtils; 33 import com.badlogic.gdx.utils.XmlReader; 34 import com.badlogic.gdx.utils.XmlReader.Element; 35 36 public abstract class BaseTmxMapLoader<P extends AssetLoaderParameters<TiledMap>> extends AsynchronousAssetLoader<TiledMap, P> { 37 38 public static class Parameters extends AssetLoaderParameters<TiledMap> { 39 /** generate mipmaps? **/ 40 public boolean generateMipMaps = false; 41 /** The TextureFilter to use for minification **/ 42 public TextureFilter textureMinFilter = TextureFilter.Nearest; 43 /** The TextureFilter to use for magnification **/ 44 public TextureFilter textureMagFilter = TextureFilter.Nearest; 45 /** Whether to convert the objects' pixel position and size to the equivalent in tile space. **/ 46 public boolean convertObjectToTileSpace = false; 47 /** Whether to flip all Y coordinates so that Y positive is down. All LibGDX renderers require flipped Y coordinates, and 48 * thus flipY set to true. This parameter is included for non-rendering related purposes of TMX files, or custom renderers. */ 49 public boolean flipY = true; 50 } 51 52 protected static final int FLAG_FLIP_HORIZONTALLY = 0x80000000; 53 protected static final int FLAG_FLIP_VERTICALLY = 0x40000000; 54 protected static final int FLAG_FLIP_DIAGONALLY = 0x20000000; 55 protected static final int MASK_CLEAR = 0xE0000000; 56 57 protected XmlReader xml = new XmlReader(); 58 protected Element root; 59 protected boolean convertObjectToTileSpace; 60 protected boolean flipY = true; 61 62 protected int mapTileWidth; 63 protected int mapTileHeight; 64 protected int mapWidthInPixels; 65 protected int mapHeightInPixels; 66 67 protected TiledMap map; 68 69 public BaseTmxMapLoader (FileHandleResolver resolver) { 70 super(resolver); 71 } 72 73 protected void loadTileLayer (TiledMap map, Element element) { 74 if (element.getName().equals("layer")) { 75 int width = element.getIntAttribute("width", 0); 76 int height = element.getIntAttribute("height", 0); 77 int tileWidth = element.getParent().getIntAttribute("tilewidth", 0); 78 int tileHeight = element.getParent().getIntAttribute("tileheight", 0); 79 TiledMapTileLayer layer = new TiledMapTileLayer(width, height, tileWidth, tileHeight); 80 81 loadBasicLayerInfo(layer, element); 82 83 int[] ids = getTileIds(element, width, height); 84 TiledMapTileSets tilesets = map.getTileSets(); 85 for (int y = 0; y < height; y++) { 86 for (int x = 0; x < width; x++) { 87 int id = ids[y * width + x]; 88 boolean flipHorizontally = ((id & FLAG_FLIP_HORIZONTALLY) != 0); 89 boolean flipVertically = ((id & FLAG_FLIP_VERTICALLY) != 0); 90 boolean flipDiagonally = ((id & FLAG_FLIP_DIAGONALLY) != 0); 91 92 TiledMapTile tile = tilesets.getTile(id & ~MASK_CLEAR); 93 if (tile != null) { 94 Cell cell = createTileLayerCell(flipHorizontally, flipVertically, flipDiagonally); 95 cell.setTile(tile); 96 layer.setCell(x, flipY ? height - 1 - y : y, cell); 97 } 98 } 99 } 100 101 Element properties = element.getChildByName("properties"); 102 if (properties != null) { 103 loadProperties(layer.getProperties(), properties); 104 } 105 map.getLayers().add(layer); 106 } 107 } 108 109 protected void loadObjectGroup (TiledMap map, Element element) { 110 if (element.getName().equals("objectgroup")) { 111 String name = element.getAttribute("name", null); 112 MapLayer layer = new MapLayer(); 113 layer.setName(name); 114 Element properties = element.getChildByName("properties"); 115 if (properties != null) { 116 loadProperties(layer.getProperties(), properties); 117 } 118 119 for (Element objectElement : element.getChildrenByName("object")) { 120 loadObject(map, layer, objectElement); 121 } 122 123 map.getLayers().add(layer); 124 } 125 } 126 127 protected void loadImageLayer (TiledMap map, Element element, FileHandle tmxFile, ImageResolver imageResolver) { 128 if (element.getName().equals("imagelayer")) { 129 int x = Integer.parseInt(element.getAttribute("x", "0")); 130 int y = Integer.parseInt(element.getAttribute("y", "0")); 131 132 if (flipY) y = mapHeightInPixels - y; 133 134 TextureRegion texture = null; 135 136 Element image = element.getChildByName("image"); 137 138 if (image != null) { 139 String source = image.getAttribute("source"); 140 FileHandle handle = getRelativeFileHandle(tmxFile, source); 141 texture = imageResolver.getImage(handle.path()); 142 y -= texture.getRegionHeight(); 143 } 144 145 TiledMapImageLayer layer = new TiledMapImageLayer(texture, x, y); 146 147 loadBasicLayerInfo(layer, element); 148 149 Element properties = element.getChildByName("properties"); 150 if (properties != null) { 151 loadProperties(layer.getProperties(), properties); 152 } 153 154 map.getLayers().add(layer); 155 } 156 } 157 158 protected void loadBasicLayerInfo (MapLayer layer, Element element) { 159 String name = element.getAttribute("name", null); 160 float opacity = Float.parseFloat(element.getAttribute("opacity", "1.0")); 161 boolean visible = element.getIntAttribute("visible", 1) == 1; 162 163 layer.setName(name); 164 layer.setOpacity(opacity); 165 layer.setVisible(visible); 166 } 167 168 protected void loadObject (TiledMap map, MapLayer layer, Element element) { 169 if (element.getName().equals("object")) { 170 MapObject object = null; 171 172 float scaleX = convertObjectToTileSpace ? 1.0f / mapTileWidth : 1.0f; 173 float scaleY = convertObjectToTileSpace ? 1.0f / mapTileHeight : 1.0f; 174 175 float x = element.getFloatAttribute("x", 0) * scaleX; 176 float y = (flipY ? (mapHeightInPixels - element.getFloatAttribute("y", 0)) : element.getFloatAttribute("y", 0)) * scaleY; 177 178 float width = element.getFloatAttribute("width", 0) * scaleX; 179 float height = element.getFloatAttribute("height", 0) * scaleY; 180 181 if (element.getChildCount() > 0) { 182 Element child = null; 183 if ((child = element.getChildByName("polygon")) != null) { 184 String[] points = child.getAttribute("points").split(" "); 185 float[] vertices = new float[points.length * 2]; 186 for (int i = 0; i < points.length; i++) { 187 String[] point = points[i].split(","); 188 vertices[i * 2] = Float.parseFloat(point[0]) * scaleX; 189 vertices[i * 2 + 1] = Float.parseFloat(point[1]) * scaleY * (flipY ? -1 : 1); 190 } 191 Polygon polygon = new Polygon(vertices); 192 polygon.setPosition(x, y); 193 object = new PolygonMapObject(polygon); 194 } else if ((child = element.getChildByName("polyline")) != null) { 195 String[] points = child.getAttribute("points").split(" "); 196 float[] vertices = new float[points.length * 2]; 197 for (int i = 0; i < points.length; i++) { 198 String[] point = points[i].split(","); 199 vertices[i * 2] = Float.parseFloat(point[0]) * scaleX; 200 vertices[i * 2 + 1] = Float.parseFloat(point[1]) * scaleY * (flipY ? -1 : 1); 201 } 202 Polyline polyline = new Polyline(vertices); 203 polyline.setPosition(x, y); 204 object = new PolylineMapObject(polyline); 205 } else if ((child = element.getChildByName("ellipse")) != null) { 206 object = new EllipseMapObject(x, flipY ? y - height : y, width, height); 207 } 208 } 209 if (object == null) { 210 String gid = null; 211 if ((gid = element.getAttribute("gid", null)) != null) { 212 int id = (int)Long.parseLong(gid); 213 boolean flipHorizontally = ((id & FLAG_FLIP_HORIZONTALLY) != 0); 214 boolean flipVertically = ((id & FLAG_FLIP_VERTICALLY) != 0); 215 216 TiledMapTile tile = map.getTileSets().getTile(id & ~MASK_CLEAR); 217 TiledMapTileMapObject tiledMapTileMapObject = new TiledMapTileMapObject(tile, flipHorizontally, flipVertically); 218 TextureRegion textureRegion = tiledMapTileMapObject.getTextureRegion(); 219 tiledMapTileMapObject.getProperties().put("gid", id); 220 tiledMapTileMapObject.setX(x); 221 tiledMapTileMapObject.setY(flipY ? y : y - height); 222 float objectWidth = element.getFloatAttribute("width", textureRegion.getRegionWidth()); 223 float objectHeight = element.getFloatAttribute("height", textureRegion.getRegionHeight()); 224 tiledMapTileMapObject.setScaleX(scaleX * (objectWidth / textureRegion.getRegionWidth())); 225 tiledMapTileMapObject.setScaleY(scaleY * (objectHeight / textureRegion.getRegionHeight())); 226 tiledMapTileMapObject.setRotation(element.getFloatAttribute("rotation", 0)); 227 object = tiledMapTileMapObject; 228 } else { 229 object = new RectangleMapObject(x, flipY ? y - height : y, width, height); 230 } 231 } 232 object.setName(element.getAttribute("name", null)); 233 String rotation = element.getAttribute("rotation", null); 234 if (rotation != null) { 235 object.getProperties().put("rotation", Float.parseFloat(rotation)); 236 } 237 String type = element.getAttribute("type", null); 238 if (type != null) { 239 object.getProperties().put("type", type); 240 } 241 int id = element.getIntAttribute("id", 0); 242 if (id != 0) { 243 object.getProperties().put("id", id); 244 } 245 object.getProperties().put("x", x); 246 247 if (object instanceof TiledMapTileMapObject) { 248 object.getProperties().put("y", y); 249 } else { 250 object.getProperties().put("y", (flipY ? y - height : y)); 251 } 252 object.getProperties().put("width", width); 253 object.getProperties().put("height", height); 254 object.setVisible(element.getIntAttribute("visible", 1) == 1); 255 Element properties = element.getChildByName("properties"); 256 if (properties != null) { 257 loadProperties(object.getProperties(), properties); 258 } 259 layer.getObjects().add(object); 260 } 261 } 262 263 protected void loadProperties (MapProperties properties, Element element) { 264 if (element == null) return; 265 if (element.getName().equals("properties")) { 266 for (Element property : element.getChildrenByName("property")) { 267 String name = property.getAttribute("name", null); 268 String value = property.getAttribute("value", null); 269 if (value == null) { 270 value = property.getText(); 271 } 272 properties.put(name, value); 273 } 274 } 275 } 276 277 protected Cell createTileLayerCell (boolean flipHorizontally, boolean flipVertically, boolean flipDiagonally) { 278 Cell cell = new Cell(); 279 if (flipDiagonally) { 280 if (flipHorizontally && flipVertically) { 281 cell.setFlipHorizontally(true); 282 cell.setRotation(Cell.ROTATE_270); 283 } else if (flipHorizontally) { 284 cell.setRotation(Cell.ROTATE_270); 285 } else if (flipVertically) { 286 cell.setRotation(Cell.ROTATE_90); 287 } else { 288 cell.setFlipVertically(true); 289 cell.setRotation(Cell.ROTATE_270); 290 } 291 } else { 292 cell.setFlipHorizontally(flipHorizontally); 293 cell.setFlipVertically(flipVertically); 294 } 295 return cell; 296 } 297 298 static public int[] getTileIds (Element element, int width, int height) { 299 Element data = element.getChildByName("data"); 300 String encoding = data.getAttribute("encoding", null); 301 if (encoding == null) { // no 'encoding' attribute means that the encoding is XML 302 throw new GdxRuntimeException("Unsupported encoding (XML) for TMX Layer Data"); 303 } 304 int[] ids = new int[width * height]; 305 if (encoding.equals("csv")) { 306 String[] array = data.getText().split(","); 307 for (int i = 0; i < array.length; i++) 308 ids[i] = (int)Long.parseLong(array[i].trim()); 309 } else { 310 if (true) 311 if (encoding.equals("base64")) { 312 InputStream is = null; 313 try { 314 String compression = data.getAttribute("compression", null); 315 byte[] bytes = Base64Coder.decode(data.getText()); 316 if (compression == null) 317 is = new ByteArrayInputStream(bytes); 318 else if (compression.equals("gzip")) 319 is = new BufferedInputStream(new GZIPInputStream(new ByteArrayInputStream(bytes), bytes.length)); 320 else if (compression.equals("zlib")) 321 is = new BufferedInputStream(new InflaterInputStream(new ByteArrayInputStream(bytes))); 322 else 323 throw new GdxRuntimeException("Unrecognised compression (" + compression + ") for TMX Layer Data"); 324 325 byte[] temp = new byte[4]; 326 for (int y = 0; y < height; y++) { 327 for (int x = 0; x < width; x++) { 328 int read = is.read(temp); 329 while (read < temp.length) { 330 int curr = is.read(temp, read, temp.length - read); 331 if (curr == -1) break; 332 read += curr; 333 } 334 if (read != temp.length) 335 throw new GdxRuntimeException("Error Reading TMX Layer Data: Premature end of tile data"); 336 ids[y * width + x] = unsignedByteToInt(temp[0]) | unsignedByteToInt(temp[1]) << 8 337 | unsignedByteToInt(temp[2]) << 16 | unsignedByteToInt(temp[3]) << 24; 338 } 339 } 340 } catch (IOException e) { 341 throw new GdxRuntimeException("Error Reading TMX Layer Data - IOException: " + e.getMessage()); 342 } finally { 343 StreamUtils.closeQuietly(is); 344 } 345 } else { 346 // any other value of 'encoding' is one we're not aware of, probably a feature of a future version of Tiled 347 // or another editor 348 throw new GdxRuntimeException("Unrecognised encoding (" + encoding + ") for TMX Layer Data"); 349 } 350 } 351 return ids; 352 } 353 354 protected static int unsignedByteToInt (byte b) { 355 return b & 0xFF; 356 } 357 358 protected static FileHandle getRelativeFileHandle (FileHandle file, String path) { 359 StringTokenizer tokenizer = new StringTokenizer(path, "\\/"); 360 FileHandle result = file.parent(); 361 while (tokenizer.hasMoreElements()) { 362 String token = tokenizer.nextToken(); 363 if (token.equals("..")) 364 result = result.parent(); 365 else { 366 result = result.child(token); 367 } 368 } 369 return result; 370 } 371 372 } 373