# -*- coding: utf-8 -*-
"""
TileMap loader for python for Tiled, a generic tile map editor
from http://mapeditor.org/ .
It loads the \*.tmx files produced by Tiled.
"""
# Versioning scheme based on: http://en.wikipedia.org/wiki/Versioning#Designating_development_stage
#
# +-- api change, probably incompatible with older versions
# | +-- enhancements but no api change
# | |
# major.minor[.build[.revision]]
# |
# +-|* 0 for alpha (status)
# |* 1 for beta (status)
# |* 2 for release candidate
# |* 3 for (public) release
#
# For instance:
# * 1.2.0.1 instead of 1.2-a
# * 1.2.1.2 instead of 1.2-b2 (beta with some bug fixes)
# * 1.2.2.3 instead of 1.2-rc (release candidate)
# * 1.2.3.0 instead of 1.2-r (commercial distribution)
# * 1.2.3.5 instead of 1.2-r5 (commercial distribution with many bug fixes)
__revision__ = "$Rev: 115 $"
__version__ = "3.1.0." + __revision__[6:-2]
__author__ = 'DR0ID @ 2009-2011'
# import logging
# #the following few lines are needed to use logging if this module used without
# # a previous call to logging.basicConfig()
# if 0 == len(logging.root.handlers):
# logging.basicConfig(level=logging.DEBUG)
# _LOGGER = logging.getLogger('tiledtmxloader')
# if __debug__:
# _LOGGER.debug('%s loading ...' % (__name__))
# -----------------------------------------------------------------------------
import sys
from xml.dom import minidom, Node
try:
import StringIO
from StringIO import StringIO
except:
from io import StringIO
import os.path
import struct
import array
# -----------------------------------------------------------------------------
class TileMap(object):
"""
The TileMap holds all the map data.
:Ivariables:
orientation : string
orthogonal or isometric or hexagonal or shifted
tilewidth : int
width of the tiles (for all layers)
tileheight : int
height of the tiles (for all layers)
width : int
width of the map (number of tiles)
height : int
height of the map (number of tiles)
version : string
version of the map format
tile_sets : list
list of TileSet
properties : dict
the propertis set in the editor, name-value pairs, strings
pixel_width : int
width of the map in pixels
pixel_height : int
height of the map in pixels
layers : list
list of TileLayer
map_file_name : dict
file name of the map
named_layers : dict of string:TledLayer
dict containing {name : TileLayer}
named_tile_sets : dict
dict containing {name : TileSet}
"""
def __init__(self):
# This is the top container for all data. The gid is the global id
# (for a image).
# Before calling convert most of the values are strings. Some additional
# values are also calculated, see convert() for details. After calling
# convert, most values are integers or floats where appropriat.
"""
The TileMap holds all the map data.
"""
# set through parser
self.orientation = None
self.tileheight = 0
self.tilewidth = 0
self.width = 0
self.height = 0
self.version = 0
self.tile_sets = [] # TileSet
# ISSUE 9: object groups should be in the same order as layers
self.layers = [] # WorldTileLayer <- what order? back to front (guessed)
# self.object_groups = []
self.properties = {} # {name: value}
# additional info
self.pixel_width = 0
self.pixel_height = 0
self.named_layers = {} # {name: layer}
self.named_tile_sets = {} # {name: tile_set}
self.map_file_name = ""
def convert(self):
"""
Converts numerical values from strings to numerical values.
It also calculates or set additional data:
pixel_width
pixel_height
named_layers
named_tile_sets
"""
self.tilewidth = int(self.tilewidth)
self.tileheight = int(self.tileheight)
self.width = int(self.width)
self.height = int(self.height)
self.pixel_width = self.width * self.tilewidth
self.pixel_height = self.height * self.tileheight
for layer in self.layers:
# ISSUE 9
if not layer.is_object_group:
layer.tilewidth = self.tilewidth
layer.tileheight = self.tileheight
self.named_layers[layer.name] = layer
layer.convert()
for tile_set in self.tile_sets:
self.named_tile_sets[tile_set.name] = tile_set
tile_set.spacing = int(tile_set.spacing)
tile_set.margin = int(tile_set.margin)
for img in tile_set.images:
if img.trans:
img.trans = (int(img.trans[:2], 16), \
int(img.trans[2:4], 16), \
int(img.trans[4:], 16))
def decode(self):
"""
Decodes the TileLayer encoded_content and saves it in decoded_content.
"""
for layer in self.layers:
if not layer.is_object_group:
layer.decode()
# -----------------------------------------------------------------------------
class TileSet(object):
"""
A tileset holds the tiles and its images.
:Ivariables:
firstgid : int
the first gid of this tileset
name : string
the name of this TileSet
images : list
list of TileImages
tiles : list
list of Tiles
indexed_images : dict
after calling load() it is dict containing id: image
spacing : int
the spacing between tiles
marging : int
the marging of the tiles
properties : dict
the propertis set in the editor, name-value pairs
tilewidth : int
the actual width of the tile, can be different from the tilewidth
of the map
tilehight : int
the actual hight of th etile, can be different from the tilehight
of the map
"""
def __init__(self):
self.firstgid = 0
self.name = None
self.images = [] # TileImage
self.tiles = [] # Tile
self.indexed_images = {} # {id:image}
self.spacing = 0
self.margin = 0
self.properties = {}
self.tileheight = 0
self.tilewidth = 0
# -----------------------------------------------------------------------------
class TileImage(object):
"""
An image of a tile or just an image.
:Ivariables:
id : int
id of this image (has nothing to do with gid)
format : string
the format as string, only 'png' at the moment
source : string
filename of the image. either this is set or the content
encoding : string
encoding of the content
trans : tuple of (r,g,b)
the colorkey color, raw as hex, after calling convert just a
(r,g,b) tuple
properties : dict
the propertis set in the editor, name-value pairs
image : TileImage
after calling load the pygame surface
"""
def __init__(self):
self.id = 0
self.format = None
self.source = None
self.encoding = None # from ...
self.content = None # from ...
self.image = None
self.trans = None
self.properties = {} # {name: value}
# -----------------------------------------------------------------------------
class Tile(object):
"""
A single tile.
:Ivariables:
id : int
id of the tile gid = TileSet.firstgid + Tile.id
images : list of :class:TileImage
list of TileImage, either its 'id' or 'image data' will be set
properties : dict of name:value
the propertis set in the editor, name-value pairs
"""
# [20:22] DR0ID_: to sum up: there are two use cases,
# if the tile element has a child element 'image' then tile is
# standalone with its own id and
# the other case where a tileset is present then it
# referes to the image with that id in the tileset
def __init__(self):
self.id = 0
self.images = [] # uses TileImage but either only id will be set or image data
self.properties = {} # {name: value}
# -----------------------------------------------------------------------------
class TileLayer(object):
"""
A layer of the world.
:Ivariables:
x : int
position of layer in the world in number of tiles (not pixels)
y : int
position of layer in the world in number of tiles (not pixels)
width : int
number of tiles in x direction
height : int
number of tiles in y direction
pixel_width : int
width of layer in pixels
pixel_height : int
height of layer in pixels
name : string
name of this layer
opacity : float
float from 0 (full transparent) to 1.0 (opaque)
decoded_content : list
list of graphics id going through the map::
e.g [1, 1, 1, ]
where decoded_content[0] is (0,0)
decoded_content[1] is (1,0)
...
decoded_content[w] is (width,0)
decoded_content[w+1] is (0,1)
...
decoded_content[w * h] is (width,height)
usage: graphics id = decoded_content[tile_x + tile_y * width]
content2D : list
list of list, usage: graphics id = content2D[x][y]
"""
def __init__(self):
self.width = 0
self.height = 0
self.x = 0
self.y = 0
self.pixel_width = 0
self.pixel_height = 0
self.name = None
self.opacity = -1
self.encoding = None
self.compression = None
self.encoded_content = None
self.decoded_content = []
self.visible = True
self.properties = {} # {name: value}
self.content2D = None
self.is_object_group = False # ISSUE 9
def decode(self):
"""
Converts the contents in a list of integers which are the gid of the
used tiles. If necessairy it decodes and uncompresses the contents.
"""
self.decoded_content = []
if self.encoded_content:
content = self.encoded_content
if self.encoding:
if self.encoding.lower() == 'base64':
content = decode_base64(content)
elif self.encoding.lower() == 'csv':
list_of_lines = content.split()
for line in list_of_lines:
self.decoded_content.extend(line.split(','))
self.decoded_content = list(map(int, \
[val for val in self.decoded_content if val]))
content = ""
else:
raise Exception('unknown data encoding %s' % \
(self.encoding))
else:
# in the case of xml the encoded_content already contains a
# list of integers
self.decoded_content = list(map(int, self.encoded_content))
content = ""
if self.compression:
if self.compression == 'gzip':
content = decompress_gzip(content)
elif self.compression == 'zlib':
content = decompress_zlib(content)
else:
raise Exception('unknown data compression %s' % \
(self.compression))
else:
raise Exception('no encoded content to decode')
struc = struct.Struct("<" + "I" * self.width)
struc_unpack_from = struc.unpack_from
self_decoded_content_extend = self.decoded_content.extend
for idx in range(0, len(content), 4 * self.width):
val = struc_unpack_from(content, idx)
self_decoded_content_extend(val)
arr = array.array('I')
arr.fromlist(self.decoded_content)
self.decoded_content = arr
# TODO: generate property grid here??
self._gen_2D()
def _gen_2D(self):
self.content2D = []
# generate the needed lists and fill them
for xpos in range(self.width):
self.content2D.append(array.array('I'))
for ypos in range(self.height):
self.content2D[xpos].append( \
self.decoded_content[xpos + ypos * self.width])
def pretty_print(self):
num = 0
for y in range(int(self.height)):
output = ""
for x in range(int(self.width)):
output += str(self.decoded_content[num])
num += 1
print(output)
def convert(self):
self.opacity = float(self.opacity)
self.x = int(self.x)
self.y = int(self.y)
self.width = int(self.width)
self.height = int(self.height)
self.pixel_width = self.width * self.tilewidth
self.pixel_height = self.height * self.tileheight
self.visible = bool(int(self.visible))
# def get_visible_tile_range(self, xmin, ymin, xmax, ymax):
# tile_w = self.pixel_width / self.width
# tile_h = self.pixel_height / self.height
# left = int(round(float(xmin) / tile_w)) - 1
# right = int(round(float(xmax) / tile_w)) + 2
# top = int(round(float(ymin) / tile_h)) - 1
# bottom = int(round(float(ymax) / tile_h)) + 2
# return (left, top, left - right, top - bottom)
# def get_tiles(self, xmin, ymin, xmax, ymax):
# tiles = []
# if self.visible:
# for ypos in range(ymin, ymax):
# for xpos in range(xmin, xmax):
# try:
# img_idx = self.content2D[xpos][ypos]
# if img_idx:
# tiles.append((xpos, ypos, img_idx))
# except IndexError:
# pass
# return tiles
# -----------------------------------------------------------------------------
class MapObjectGroupLayer(object):
"""
Group of objects on the map.
:Ivariables:
x : int
the x position
y : int
the y position
width : int
width of the bounding box (usually 0, so no use)
height : int
height of the bounding box (usually 0, so no use)
name : string
name of the group
objects : list
list of the map objects
"""
def __init__(self):
self.width = 0
self.height = 0
self.name = None
self.objects = []
self.x = 0
self.y = 0
self.visible = True
self.properties = {} # {name: value}
self.is_object_group = True # ISSUE 9
def convert(self):
self.x = int(self.x)
self.y = int(self.y)
self.width = int(self.width)
self.height = int(self.height)
for map_obj in self.objects:
map_obj.x = int(float(map_obj.x))
map_obj.y = int(float(map_obj.y))
map_obj.width = int(float(map_obj.width))
map_obj.height = int(float(map_obj.height))
# -----------------------------------------------------------------------------
class MapObject(object):
"""
A single object on the map.
:Ivariables:
x : int
x position relative to group x position
y : int
y position relative to group y position
width : int
width of this object
height : int
height of this object
type : string
the type of this object
image_source : string
source path of the image for this object
image : :class:TileImage
after loading this is the pygame surface containing the image
"""
def __init__(self):
self.name = None
self.x = 0
self.y = 0
self.width = 0
self.height = 0
self.type = None
self.image_source = None
self.image = None
self.properties = {} # {name: value}
# -----------------------------------------------------------------------------
def decode_base64(in_str):
"""
Decodes a base64 string and returns it.
:Parameters:
in_str : string
base64 encoded string
:returns: decoded string
"""
import base64
return base64.decodestring(in_str.encode('latin-1'))
# -----------------------------------------------------------------------------
def decompress_gzip(in_str):
"""
Uncompresses a gzip string and returns it.
:Parameters:
in_str : string
gzip compressed string
:returns: uncompressed string
"""
import gzip
if sys.version_info > (2, ):
from io import BytesIO
copmressed_stream = BytesIO(in_str)
else:
# gzip can only handle file object therefore using StringIO
copmressed_stream = StringIO(in_str.decode("latin-1"))
gzipper = gzip.GzipFile(fileobj=copmressed_stream)
content = gzipper.read()
gzipper.close()
return content
# -----------------------------------------------------------------------------
def decompress_zlib(in_str):
"""
Uncompresses a zlib string and returns it.
:Parameters:
in_str : string
zlib compressed string
:returns: uncompressed string
"""
import zlib
content = zlib.decompress(in_str)
return content
# -----------------------------------------------------------------------------
def printer(obj, ident=''):
"""
Helper function, prints a hirarchy of objects.
"""
import inspect
print(ident + obj.__class__.__name__.upper())
ident += ' '
lists = []
for name in dir(obj):
elem = getattr(obj, name)
if isinstance(elem, list) and name != 'decoded_content':
lists.append(elem)
elif not inspect.ismethod(elem):
if not name.startswith('__'):
if name == 'data' and elem:
print(ident + 'data = ')
printer(elem, ident + ' ')
else:
print(ident + '%s\t= %s' % (name, getattr(obj, name)))
for objt_list in lists:
for _obj in objt_list:
printer(_obj, ident + ' ')
# -----------------------------------------------------------------------------
class VersionError(Exception): pass
# -----------------------------------------------------------------------------
class TileMapParser(object):
"""
Allows to parse and decode map files for 'Tiled', a open source map editor
written in java. It can be found here: http://mapeditor.org/
"""
def _build_tile_set(self, tile_set_node, world_map):
tile_set = TileSet()
self._set_attributes(tile_set_node, tile_set)
if hasattr(tile_set, "source"):
tile_set = self._parse_tsx(tile_set.source, tile_set, world_map)
else:
tile_set = self._get_tile_set(tile_set_node, tile_set, \
self.map_file_name)
world_map.tile_sets.append(tile_set)
def _parse_tsx(self, file_name, tile_set, world_map):
# ISSUE 5: the *.tsx file is probably relative to the *.tmx file
if not os.path.isabs(file_name):
# print "map file name", self.map_file_name
file_name = self._get_abs_path(self.map_file_name, file_name)
# print "tsx filename: ", file_name
# would be more elegant to use "with open(file_name, "rb") as file:"
# but that is python 2.6
file = None
try:
file = open(file_name, "rb")
dom = minidom.parseString(file.read())
finally:
if file:
file.close()
for node in self._get_nodes(dom.childNodes, 'tileset'):
tile_set = self._get_tile_set(node, tile_set, file_name)
break
return tile_set
def _get_tile_set(self, tile_set_node, tile_set, base_path):
for node in self._get_nodes(tile_set_node.childNodes, 'image'):
self._build_tile_set_image(node, tile_set, base_path)
for node in self._get_nodes(tile_set_node.childNodes, 'tile'):
self._build_tile_set_tile(node, tile_set)
self._set_attributes(tile_set_node, tile_set)
return tile_set
def _build_tile_set_image(self, image_node, tile_set, base_path):
image = TileImage()
self._set_attributes(image_node, image)
# id of TileImage has to be set! -> Tile.TileImage will only have id set
for node in self._get_nodes(image_node.childNodes, 'data'):
self._set_attributes(node, image)
image.content = node.childNodes[0].nodeValue
image.source = self._get_abs_path(base_path, image.source) # ISSUE 5
tile_set.images.append(image)
def _get_abs_path(self, base, relative):
if os.path.isabs(relative):
return relative
if os.path.isfile(base):
base = os.path.dirname(base)
return os.path.abspath(os.path.join(base, relative))
def _build_tile_set_tile(self, tile_set_node, tile_set):
tile = Tile()
self._set_attributes(tile_set_node, tile)
for node in self._get_nodes(tile_set_node.childNodes, 'image'):
self._build_tile_set_tile_image(node, tile)
tile_set.tiles.append(tile)
def _build_tile_set_tile_image(self, tile_node, tile):
tile_image = TileImage()
self._set_attributes(tile_node, tile_image)
for node in self._get_nodes(tile_node.childNodes, 'data'):
self._set_attributes(node, tile_image)
tile_image.content = node.childNodes[0].nodeValue
tile.images.append(tile_image)
def _build_layer(self, layer_node, world_map):
layer = TileLayer()
self._set_attributes(layer_node, layer)
for node in self._get_nodes(layer_node.childNodes, 'data'):
self._set_attributes(node, layer)
if layer.encoding:
layer.encoded_content = node.lastChild.nodeValue
else:
#print 'has childnodes', node.hasChildNodes()
layer.encoded_content = []
for child in node.childNodes:
if child.nodeType == Node.ELEMENT_NODE and \
child.nodeName == "tile":
val = child.attributes["gid"].nodeValue
#print child, val
layer.encoded_content.append(val)
world_map.layers.append(layer)
def _build_world_map(self, world_node):
world_map = TileMap()
self._set_attributes(world_node, world_map)
if world_map.version != "1.0":
raise VersionError('this parser was made for maps of version 1.0, found version %s' % world_map.version)
for node in self._get_nodes(world_node.childNodes, 'tileset'):
self._build_tile_set(node, world_map)
for node in self._get_nodes(world_node.childNodes, 'layer'):
self._build_layer(node, world_map)
for node in self._get_nodes(world_node.childNodes, 'objectgroup'):
self._build_object_groups(node, world_map)
return world_map
def _build_object_groups(self, object_group_node, world_map):
object_group = MapObjectGroupLayer()
self._set_attributes(object_group_node, object_group)
for node in self._get_nodes(object_group_node.childNodes, 'object'):
tiled_object = MapObject()
self._set_attributes(node, tiled_object)
for p in self._get_nodes(node.childNodes, 'polyline'):
def xy(s):
(x, y) = [int(c) for c in s.split(",")]
return (int(tiled_object.x) + x, int(tiled_object.y) + y)
tiled_object.polyline = \
[xy(s) for s in p.attributes['points'].nodeValue.split()]
for img_node in self._get_nodes(node.childNodes, 'image'):
tiled_object.image_source = \
img_node.attributes['source'].nodeValue
object_group.objects.append(tiled_object)
# ISSUE 9
world_map.layers.append(object_group)
# -- helpers -- #
def _get_nodes(self, nodes, name):
for node in nodes:
if node.nodeType == Node.ELEMENT_NODE and node.nodeName == name:
yield node
def _set_attributes(self, node, obj):
attrs = node.attributes
for attr_name in list(attrs.keys()):
setattr(obj, attr_name, attrs.get(attr_name).nodeValue)
self._get_properties(node, obj)
def _get_properties(self, node, obj):
props = {}
for properties_node in self._get_nodes(node.childNodes, 'properties'):
for property_node in self._get_nodes(properties_node.childNodes, 'property'):
try:
props[property_node.attributes['name'].nodeValue] = \
property_node.attributes['value'].nodeValue
except KeyError:
props[property_node.attributes['name'].nodeValue] = \
property_node.lastChild.nodeValue
obj.properties.update(props)
# -- parsers -- #
def parse(self, file_name):
"""
Parses the given map. Does no decoding nor loading of the data.
:return: instance of TileMap
"""
# would be more elegant to use
# "with open(file_name, "rb") as tmx_file:" but that is python 2.6
self.map_file_name = os.path.abspath(file_name)
tmx_file = None
try:
tmx_file = open(self.map_file_name, "rb")
dom = minidom.parseString(tmx_file.read())
finally:
if tmx_file:
tmx_file.close()
for node in self._get_nodes(dom.childNodes, 'map'):
world_map = self._build_world_map(node)
break
world_map.map_file_name = self.map_file_name
world_map.convert()
return world_map
def parse_decode(self, file_name):
"""
Parses the map but additionally decodes the data.
:return: instance of TileMap
"""
world_map = self.parse(file_name)
world_map.decode()
return world_map
# -----------------------------------------------------------------------------
class AbstractResourceLoader(object):
"""
Abstract base class for the resource loader.
"""
FLIP_X = 1 << 31
FLIP_Y = 1 << 30
def __init__(self):
self.indexed_tiles = {} # {gid: (offsetx, offsety, image}
self.world_map = None
self._img_cache = {}
def _load_image(self, filename, colorkey=None): # -> image
"""
Load a single image.
:Parameters:
filename : string
Path to the file to be loaded.
colorkey : tuple
The (r, g, b) color that should be used as colorkey
(or magic color).
Default: None
:rtype: image
"""
raise NotImplementedError('This should be implemented in a inherited class')
def _load_image_file_like(self, file_like_obj, colorkey=None): # -> image
"""
Load a image from a file like object.
:Parameters:
file_like_obj : file
This is the file like object to load the image from.
colorkey : tuple
The (r, g, b) color that should be used as colorkey
(or magic color).
Default: None
:rtype: image
"""
raise NotImplementedError('This should be implemented in a inherited class')
def _load_image_parts(self, filename, margin, spacing, tilewidth, tileheight, colorkey=None): #-> [images]
"""
Load different tile images from one source image.
:Parameters:
filename : string
Path to image to be loaded.
margin : int
The margin around the image.
spacing : int
The space between the tile images.
tilewidth : int
The width of a single tile.
tileheight : int
The height of a single tile.
colorkey : tuple
The (r, g, b) color that should be used as colorkey
(or magic color).
Default: None
Luckily that iteration is so easy in python::
...
w, h = image_size
for y in xrange(margin, h, tileheight + spacing):
for x in xrange(margin, w, tilewidth + spacing):
...
:rtype: a list of images
"""
raise NotImplementedError('This should be implemented in a inherited class')
def load(self, tile_map):
"""
"""
self.world_map = tile_map
for tile_set in tile_map.tile_sets:
# do images first, because tiles could reference it
for img in tile_set.images:
if img.source:
self._load_image_from_source(tile_map, tile_set, img)
else:
tile_set.indexed_images[img.id] = self._load_tile_image(img)
# tiles
for tile in tile_set.tiles:
for img in tile.images:
if not img.content and not img.source:
# only image id set
indexed_img = tile_set.indexed_images[img.id]
self.indexed_tiles[int(tile_set.firstgid) + int(tile.id)] = (0, 0, indexed_img)
else:
if img.source:
self._load_image_from_source(tile_map, tile_set, img)
else:
indexed_img = self._load_tile_image(img)
self.indexed_tiles[int(tile_set.firstgid) + int(tile.id)] = (0, 0, indexed_img)
def _load_image_from_source(self, tile_map, tile_set, a_tile_image):
# relative path to file
img_path = os.path.join(os.path.dirname(tile_map.map_file_name), \
a_tile_image.source)
tile_width = int(tile_map.tilewidth)
tile_height = int(tile_map.tileheight)
if tile_set.tileheight:
tile_width = int(tile_set.tilewidth)
if tile_set.tilewidth:
tile_height = int(tile_set.tileheight)
offsetx = 0
offsety = 0
# the offset is used for pygame because the origin is topleft in pygame
if tile_height > tile_map.tileheight:
offsety = tile_height - tile_map.tileheight
idx = 0
for image in self._load_image_parts(img_path, \
tile_set.margin, tile_set.spacing, \
tile_width, tile_height, a_tile_image.trans):
self.indexed_tiles[int(tile_set.firstgid) + idx] = \
(offsetx, -offsety, image)
idx += 1
def _load_tile_image(self, a_tile_image):
img_str = a_tile_image.content
if a_tile_image.encoding:
if a_tile_image.encoding == 'base64':
img_str = decode_base64(a_tile_image.content)
else:
raise Exception('unknown image encoding %s' % a_tile_image.encoding)
sio = StringIO(img_str)
new_image = self._load_image_file_like(sio, a_tile_image.trans)
return new_image
# -----------------------------------------------------------------------------