Browse Source

Add support for polyline

James Bowman 6 years ago
parent
commit
f0c9faaee5
1 changed files with 917 additions and 0 deletions
  1. 917 0
      gameduino2/tmxreader.py

+ 917 - 0
gameduino2/tmxreader.py

@@ -0,0 +1,917 @@
+# -*- 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 <data>...</data>
+        self.content = None # from <data>...</data>
+        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
+
+#  -----------------------------------------------------------------------------
+