• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
BaseTmxMapLoader(FileHandleResolver resolver)69 	public BaseTmxMapLoader (FileHandleResolver resolver) {
70 		super(resolver);
71 	}
72 
loadTileLayer(TiledMap map, Element element)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 
loadObjectGroup(TiledMap map, Element element)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 
loadImageLayer(TiledMap map, Element element, FileHandle tmxFile, ImageResolver imageResolver)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 
loadBasicLayerInfo(MapLayer layer, Element element)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 
loadObject(TiledMap map, MapLayer layer, Element element)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 
loadProperties(MapProperties properties, Element element)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 
createTileLayerCell(boolean flipHorizontally, boolean flipVertically, boolean flipDiagonally)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 
getTileIds(Element element, int width, int height)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 
unsignedByteToInt(byte b)354 	protected static int unsignedByteToInt (byte b) {
355 		return b & 0xFF;
356 	}
357 
getRelativeFileHandle(FileHandle file, String path)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