Removed old file
[2dworld.git] / tiledtmxloader / tmxreader.py
blob459b89fa1c07aea149e84113e59d687eb80622fe
1 # -*- coding: utf-8 -*-
3 """
4 TileMap loader for python for Tiled, a generic tile map editor
5 from http://mapeditor.org/ .
6 It loads the \*.tmx files produced by Tiled.
9 """
11 # Versioning scheme based on: http://en.wikipedia.org/wiki/Versioning#Designating_development_stage
13 # +-- api change, probably incompatible with older versions
14 # | +-- enhancements but no api change
15 # | |
16 # major.minor[.build[.revision]]
17 # |
18 # +-|* 0 for alpha (status)
19 # |* 1 for beta (status)
20 # |* 2 for release candidate
21 # |* 3 for (public) release
23 # For instance:
24 # * 1.2.0.1 instead of 1.2-a
25 # * 1.2.1.2 instead of 1.2-b2 (beta with some bug fixes)
26 # * 1.2.2.3 instead of 1.2-rc (release candidate)
27 # * 1.2.3.0 instead of 1.2-r (commercial distribution)
28 # * 1.2.3.5 instead of 1.2-r5 (commercial distribution with many bug fixes)
30 __revision__ = "$Rev$"
31 __version__ = "3.1.0." + __revision__[6:-2]
32 __author__ = 'DR0ID @ 2009-2011'
34 # import logging
35 # #the following few lines are needed to use logging if this module used without
36 # # a previous call to logging.basicConfig()
37 # if 0 == len(logging.root.handlers):
38 # logging.basicConfig(level=logging.DEBUG)
40 # _LOGGER = logging.getLogger('tiledtmxloader')
41 # if __debug__:
42 # _LOGGER.debug('%s loading ...' % (__name__))
43 # -----------------------------------------------------------------------------
46 import sys
47 from xml.dom import minidom, Node
48 try:
49 # python 2.x
50 import StringIO
51 from StringIO import StringIO
52 except:
53 # python 3.x
54 from io import StringIO
55 import os.path
56 import struct
57 import array
59 # -----------------------------------------------------------------------------
60 class TileMap(object):
61 """
63 The TileMap holds all the map data.
65 :Ivariables:
66 orientation : string
67 orthogonal or isometric or hexagonal or shifted
68 tilewidth : int
69 width of the tiles (for all layers)
70 tileheight : int
71 height of the tiles (for all layers)
72 width : int
73 width of the map (number of tiles)
74 height : int
75 height of the map (number of tiles)
76 version : string
77 version of the map format
78 tile_sets : list
79 list of TileSet
80 properties : dict
81 the propertis set in the editor, name-value pairs, strings
82 pixel_width : int
83 width of the map in pixels
84 pixel_height : int
85 height of the map in pixels
86 layers : list
87 list of TileLayer
88 map_file_name : dict
89 file name of the map
90 named_layers : dict of string:TledLayer
91 dict containing {name : TileLayer}
92 named_tile_sets : dict
93 dict containing {name : TileSet}
95 """
98 def __init__(self):
99 # This is the top container for all data. The gid is the global id
100 # (for a image).
101 # Before calling convert most of the values are strings. Some additional
102 # values are also calculated, see convert() for details. After calling
103 # convert, most values are integers or floats where appropriat.
105 The TileMap holds all the map data.
107 # set through parser
108 self.orientation = None
109 self.tileheight = 0
110 self.tilewidth = 0
111 self.width = 0
112 self.height = 0
113 self.version = 0
114 self.tile_sets = [] # TileSet
115 # ISSUE 9: object groups should be in the same order as layers
116 self.layers = [] # WorldTileLayer <- what order? back to front (guessed)
117 # self.object_groups = []
118 self.properties = {} # {name: value}
119 # additional info
120 self.pixel_width = 0
121 self.pixel_height = 0
122 self.named_layers = {} # {name: layer}
123 self.named_tile_sets = {} # {name: tile_set}
124 self.map_file_name = ""
126 def convert(self):
128 Converts numerical values from strings to numerical values.
129 It also calculates or set additional data:
130 pixel_width
131 pixel_height
132 named_layers
133 named_tile_sets
135 self.tilewidth = int(self.tilewidth)
136 self.tileheight = int(self.tileheight)
137 self.width = int(self.width)
138 self.height = int(self.height)
139 self.pixel_width = self.width * self.tilewidth
140 self.pixel_height = self.height * self.tileheight
142 for layer in self.layers:
143 # ISSUE 9
144 if not layer.is_object_group:
145 layer.tilewidth = self.tilewidth
146 layer.tileheight = self.tileheight
147 self.named_layers[layer.name] = layer
148 layer.convert()
150 for tile_set in self.tile_sets:
151 self.named_tile_sets[tile_set.name] = tile_set
152 tile_set.spacing = int(tile_set.spacing)
153 tile_set.margin = int(tile_set.margin)
154 for img in tile_set.images:
155 if img.trans:
156 img.trans = (int(img.trans[:2], 16), \
157 int(img.trans[2:4], 16), \
158 int(img.trans[4:], 16))
160 def decode(self):
162 Decodes the TileLayer encoded_content and saves it in decoded_content.
164 for layer in self.layers:
165 if not layer.is_object_group:
166 self._decode_layer(layer)
167 layer.generate_2D()
169 def _decode_layer(self, layer):
171 Converts the contents in a list of integers which are the gid of the
172 used tiles. If necessairy it decodes and uncompresses the contents.
174 layer.decoded_content = []
175 if layer.encoded_content:
176 content = layer.encoded_content
177 if layer.encoding:
178 if layer.encoding.lower() == 'base64':
179 content = decode_base64(content)
180 elif layer.encoding.lower() == 'csv':
181 list_of_lines = content.split()
182 for line in list_of_lines:
183 layer.decoded_content.extend(line.split(','))
184 self._fill_decoded_content(layer, list(map(int, \
185 [val for val in layer.decoded_content if val])))
186 content = ""
187 else:
188 raise Exception('unknown data encoding %s' % \
189 (layer.encoding))
190 else:
191 # in the case of xml the encoded_content already contains a
192 # list of integers
193 self._fill_decoded_content(layer, list(map(int, layer.encoded_content)))
195 content = ""
196 if layer.compression:
197 if layer.compression == 'gzip':
198 content = decompress_gzip(content)
199 elif layer.compression == 'zlib':
200 content = decompress_zlib(content)
201 else:
202 raise Exception('unknown data compression %s' % \
203 (layer.compression))
204 else:
205 raise Exception('no encoded content to decode')
207 # struc = struct.Struct("<" + "I" * layer.width)
208 # struc_unpack_from = struc.unpack_from
209 # layer_decoded_content_extend = layer.decoded_content.extend
210 # for idx in range(0, len(content), 4 * layer.width):
211 # val = struc_unpack_from(content, idx)
212 # layer_decoded_content_extend(val)
213 ####
214 if content:
215 struc = struct.Struct("<" + "I" * layer.width * layer.height)
216 val = struc.unpack(content) # make Cell
217 # layer.decoded_content.extend(val)
220 # layer.decoded_content = array.array('I')
221 # layer.decoded_content.extend(val)
222 self._fill_decoded_content(layer, val)
225 # arr = array.array('I')
226 # arr.fromlist(layer.decoded_content)
227 # layer.decoded_content = arr
229 # TODO: generate property grid here??
231 def _fill_decoded_content(self, layer, gid_list):
232 layer.decoded_content = array.array('I')
233 layer.decoded_content.extend(gid_list)
238 # -----------------------------------------------------------------------------
241 class TileSet(object):
243 A tileset holds the tiles and its images.
245 :Ivariables:
246 firstgid : int
247 the first gid of this tileset
248 name : string
249 the name of this TileSet
250 images : list
251 list of TileImages
252 tiles : list
253 list of Tiles
254 indexed_images : dict
255 after calling load() it is dict containing id: image
256 spacing : int
257 the spacing between tiles
258 marging : int
259 the marging of the tiles
260 properties : dict
261 the propertis set in the editor, name-value pairs
262 tilewidth : int
263 the actual width of the tile, can be different from the tilewidth
264 of the map
265 tilehight : int
266 the actual hight of th etile, can be different from the tilehight
267 of the map
271 def __init__(self):
272 self.firstgid = 0
273 self.name = None
274 self.images = [] # TileImage
275 self.tiles = [] # Tile
276 self.indexed_images = {} # {id:image}
277 self.spacing = 0
278 self.margin = 0
279 self.properties = {}
280 self.tileheight = 0
281 self.tilewidth = 0
283 # -----------------------------------------------------------------------------
285 class TileImage(object):
287 An image of a tile or just an image.
289 :Ivariables:
290 id : int
291 id of this image (has nothing to do with gid)
292 format : string
293 the format as string, only 'png' at the moment
294 source : string
295 filename of the image. either this is set or the content
296 encoding : string
297 encoding of the content
298 trans : tuple of (r,g,b)
299 the colorkey color, raw as hex, after calling convert just a
300 (r,g,b) tuple
301 properties : dict
302 the propertis set in the editor, name-value pairs
303 image : TileImage
304 after calling load the pygame surface
307 def __init__(self):
308 self.id = -1
309 self.format = None
310 self.source = None
311 self.encoding = None # from <data>...</data>
312 self.content = None # from <data>...</data>
313 self.image = None
314 self.trans = None
315 self.properties = {} # {name: value}
317 # -----------------------------------------------------------------------------
319 class Tile(object):
321 A single tile.
323 :Ivariables:
324 id : int
325 id of the tile gid = TileSet.firstgid + Tile.id
326 images : list of :class:TileImage
327 list of TileImage, either its 'id' or 'image data' will be set
328 properties : dict of name:value
329 the propertis set in the editor, name-value pairs
332 # [20:22] DR0ID_: to sum up: there are two use cases,
333 # if the tile element has a child element 'image' then tile is
334 # standalone with its own id and
335 # the other case where a tileset is present then it
336 # referes to the image with that id in the tileset
338 def __init__(self):
339 self.id = -1
340 self.images = [] # uses TileImage but either only id will be set or image data
341 self.properties = {} # {name: value}
343 # -----------------------------------------------------------------------------
345 class Cell(object):
347 def __init__(self, idx):
348 self.idx = idx
349 self.porperties = None
351 # -----------------------------------------------------------------------------
353 class TileLayer(object):
355 A layer of the world.
357 :Ivariables:
358 x : int
359 position of layer in the world in number of tiles (not pixels)
360 y : int
361 position of layer in the world in number of tiles (not pixels)
362 width : int
363 number of tiles in x direction
364 height : int
365 number of tiles in y direction
366 pixel_width : int
367 width of layer in pixels
368 pixel_height : int
369 height of layer in pixels
370 name : string
371 name of this layer
372 opacity : float
373 float from 0 (full transparent) to 1.0 (opaque)
374 decoded_content : list
375 list of graphics id going through the map::
377 e.g [1, 1, 1, ]
378 where decoded_content[0] is (0,0)
379 decoded_content[1] is (1,0)
381 decoded_content[w] is (width,0)
382 decoded_content[w+1] is (0,1)
384 decoded_content[w * h] is (width,height)
386 usage: graphics id = decoded_content[tile_x + tile_y * width]
387 content2D : list
388 list of list, usage: graphics id = content2D[x][y]
392 def __init__(self):
393 self.width = 0
394 self.height = 0
395 self.x = 0
396 self.y = 0
397 self.pixel_width = 0
398 self.pixel_height = 0
399 self.name = None
400 self.opacity = -1
401 self.encoding = None
402 self.compression = None
403 self.encoded_content = None
404 self.decoded_content = []
405 self.visible = True
406 self.properties = {} # {name: value}
407 self.is_object_group = False # ISSUE 9
408 self._content2D = None
410 # def decode(self):
411 # """
412 # Converts the contents in a list of integers which are the gid of the
413 # used tiles. If necessairy it decodes and uncompresses the contents.
414 # """
415 # self.decoded_content = []
416 # if self.encoded_content:
417 # content = self.encoded_content
418 # if self.encoding:
419 # if self.encoding.lower() == 'base64':
420 # content = decode_base64(content)
421 # elif self.encoding.lower() == 'csv':
422 # list_of_lines = content.split()
423 # for line in list_of_lines:
424 # self.decoded_content.extend(line.split(','))
425 # self.decoded_content = list(map(int, \
426 # [val for val in self.decoded_content if val]))
427 # content = ""
428 # else:
429 # raise Exception('unknown data encoding %s' % \
430 # (self.encoding))
431 # else:
432 # # in the case of xml the encoded_content already contains a
433 # # list of integers
434 # self.decoded_content = list(map(int, self.encoded_content))
435 # content = ""
436 # if self.compression:
437 # if self.compression == 'gzip':
438 # content = decompress_gzip(content)
439 # elif self.compression == 'zlib':
440 # content = decompress_zlib(content)
441 # else:
442 # raise Exception('unknown data compression %s' % \
443 # (self.compression))
444 # else:
445 # raise Exception('no encoded content to decode')
447 # # struc = struct.Struct("<" + "I" * self.width)
448 # # struc_unpack_from = struc.unpack_from
449 # # self_decoded_content_extend = self.decoded_content.extend
450 # # for idx in range(0, len(content), 4 * self.width):
451 # # val = struc_unpack_from(content, idx)
452 # # self_decoded_content_extend(val)
453 # ####
454 # struc = struct.Struct("<" + "I" * self.width * self.height)
455 # val = struc.unpack(content) # make Cell
456 # # self.decoded_content.extend(val)
459 # self.decoded_content = array.array('I')
460 # self.decoded_content.extend(val)
463 # # arr = array.array('I')
464 # # arr.fromlist(self.decoded_content)
465 # # self.decoded_content = arr
467 # # TODO: generate property grid here??
469 # self._gen_2D()
471 def generate_2D(self):
472 self.content2D = []
474 # generate the needed lists and fill them
475 for xpos in range(self.width):
476 self.content2D.append(array.array('I'))
477 for ypos in range(self.height):
478 self.content2D[xpos].append( \
479 self.decoded_content[xpos + ypos * self.width])
481 def pretty_print(self):
482 num = 0
483 for y in range(int(self.height)):
484 output = ""
485 for x in range(int(self.width)):
486 output += str(self.decoded_content[num])
487 num += 1
488 print(output)
490 def convert(self):
491 self.opacity = float(self.opacity)
492 self.x = int(self.x)
493 self.y = int(self.y)
494 self.width = int(self.width)
495 self.height = int(self.height)
496 self.pixel_width = self.width * self.tilewidth
497 self.pixel_height = self.height * self.tileheight
498 self.visible = bool(int(self.visible))
500 # def get_visible_tile_range(self, xmin, ymin, xmax, ymax):
501 # tile_w = self.pixel_width / self.width
502 # tile_h = self.pixel_height / self.height
503 # left = int(round(float(xmin) / tile_w)) - 1
504 # right = int(round(float(xmax) / tile_w)) + 2
505 # top = int(round(float(ymin) / tile_h)) - 1
506 # bottom = int(round(float(ymax) / tile_h)) + 2
507 # return (left, top, left - right, top - bottom)
509 # def get_tiles(self, xmin, ymin, xmax, ymax):
510 # tiles = []
511 # if self.visible:
512 # for ypos in range(ymin, ymax):
513 # for xpos in range(xmin, xmax):
514 # try:
515 # img_idx = self.content2D[xpos][ypos]
516 # if img_idx:
517 # tiles.append((xpos, ypos, img_idx))
518 # except IndexError:
519 # pass
520 # return tiles
522 # -----------------------------------------------------------------------------
525 class MapObjectGroupLayer(object):
527 Group of objects on the map.
529 :Ivariables:
530 x : int
531 the x position
532 y : int
533 the y position
534 width : int
535 width of the bounding box (usually 0, so no use)
536 height : int
537 height of the bounding box (usually 0, so no use)
538 name : string
539 name of the group
540 objects : list
541 list of the map objects
545 def __init__(self):
546 self.width = 0
547 self.height = 0
548 self.name = None
549 self.objects = []
550 self.x = 0
551 self.y = 0
552 self.visible = True
553 self.properties = {} # {name: value}
554 self.is_object_group = True # ISSUE 9
556 def convert(self):
557 self.x = int(self.x)
558 self.y = int(self.y)
559 self.width = int(self.width)
560 self.height = int(self.height)
561 for map_obj in self.objects:
562 map_obj.x = int(map_obj.x)
563 map_obj.y = int(map_obj.y)
564 map_obj.width = int(map_obj.width)
565 map_obj.height = int(map_obj.height)
567 # -----------------------------------------------------------------------------
569 class MapObject(object):
571 A single object on the map.
573 :Ivariables:
574 x : int
575 x position relative to group x position
576 y : int
577 y position relative to group y position
578 width : int
579 width of this object
580 height : int
581 height of this object
582 type : string
583 the type of this object
584 image_source : string
585 source path of the image for this object
586 image : :class:TileImage
587 after loading this is the pygame surface containing the image
589 def __init__(self):
590 self.name = None
591 self.x = 0
592 self.y = 0
593 self.width = 0
594 self.height = 0
595 self.type = None
596 self.image_source = None
597 self.image = None
598 self.properties = {} # {name: value}
600 # -----------------------------------------------------------------------------
601 def decode_base64(in_str):
603 Decodes a base64 string and returns it.
605 :Parameters:
606 in_str : string
607 base64 encoded string
609 :returns: decoded string
611 import base64
612 return base64.decodestring(in_str.encode('latin-1'))
614 # -----------------------------------------------------------------------------
615 def decompress_gzip(in_str):
617 Uncompresses a gzip string and returns it.
619 :Parameters:
620 in_str : string
621 gzip compressed string
623 :returns: uncompressed string
625 import gzip
627 if sys.version_info > (2, ):
628 from io import BytesIO
629 copmressed_stream = BytesIO(in_str)
630 else:
631 # gzip can only handle file object therefore using StringIO
632 copmressed_stream = StringIO(in_str.decode("latin-1"))
633 gzipper = gzip.GzipFile(fileobj=copmressed_stream)
634 content = gzipper.read()
635 gzipper.close()
636 return content
638 # -----------------------------------------------------------------------------
639 def decompress_zlib(in_str):
641 Uncompresses a zlib string and returns it.
643 :Parameters:
644 in_str : string
645 zlib compressed string
647 :returns: uncompressed string
649 import zlib
650 content = zlib.decompress(in_str)
651 return content
652 # -----------------------------------------------------------------------------
653 def printer(obj, ident=''):
655 Helper function, prints a hirarchy of objects.
657 import inspect
658 print(ident + obj.__class__.__name__.upper())
659 ident += ' '
660 lists = []
661 for name in dir(obj):
662 elem = getattr(obj, name)
663 if isinstance(elem, list) and name != 'decoded_content':
664 lists.append(elem)
665 elif not inspect.ismethod(elem):
666 if not name.startswith('__'):
667 if name == 'data' and elem:
668 print(ident + 'data = ')
669 printer(elem, ident + ' ')
670 else:
671 print(ident + '%s\t= %s' % (name, getattr(obj, name)))
672 for objt_list in lists:
673 for _obj in objt_list:
674 printer(_obj, ident + ' ')
676 # -----------------------------------------------------------------------------
678 class VersionError(Exception): pass
680 # -----------------------------------------------------------------------------
681 class TileMapParser(object):
683 Allows to parse and decode map files for 'Tiled', a open source map editor
684 written in java. It can be found here: http://mapeditor.org/
687 def _build_tile_set(self, tile_set_node, world_map):
688 tile_set = TileSet()
689 self._set_attributes(tile_set_node, tile_set)
690 if hasattr(tile_set, "source"):
691 tile_set = self._parse_tsx(tile_set.source, tile_set, world_map)
692 else:
693 tile_set = self._get_tile_set(tile_set_node, tile_set, \
694 self.map_file_name)
695 world_map.tile_sets.append(tile_set)
697 def _parse_tsx(self, file_name, tile_set, world_map):
698 # ISSUE 5: the *.tsx file is probably relative to the *.tmx file
699 if not os.path.isabs(file_name):
700 # print "map file name", self.map_file_name
701 file_name = self._get_abs_path(self.map_file_name, file_name)
702 # print "tsx filename: ", file_name
703 # would be more elegant to use "with open(file_name, "rb") as file:"
704 # but that is python 2.6
705 file = None
706 try:
707 file = open(file_name, "rb")
708 dom = minidom.parseString(file.read())
709 finally:
710 if file:
711 file.close()
712 for node in self._get_nodes(dom.childNodes, 'tileset'):
713 tile_set = self._get_tile_set(node, tile_set, file_name)
714 break
715 return tile_set
717 def _get_tile_set(self, tile_set_node, tile_set, base_path):
718 for node in self._get_nodes(tile_set_node.childNodes, 'image'):
719 self._build_tile_set_image(node, tile_set, base_path)
720 for node in self._get_nodes(tile_set_node.childNodes, 'tile'):
721 self._build_tile_set_tile(node, tile_set)
722 self._set_attributes(tile_set_node, tile_set)
723 return tile_set
725 def _build_tile_set_image(self, image_node, tile_set, base_path):
726 image = TileImage()
727 self._set_attributes(image_node, image)
728 # id of TileImage has to be set! -> Tile.TileImage will only have id set
729 for node in self._get_nodes(image_node.childNodes, 'data'):
730 self._set_attributes(node, image)
731 image.content = node.childNodes[0].nodeValue
732 image.source = self._get_abs_path(base_path, image.source) # ISSUE 5
733 tile_set.images.append(image)
735 def _get_abs_path(self, base, relative):
736 if os.path.isabs(relative):
737 return relative
738 if os.path.isfile(base):
739 base = os.path.dirname(base)
740 return os.path.abspath(os.path.join(base, relative))
742 def _build_tile_set_tile(self, tile_set_node, tile_set):
743 tile = Tile()
744 self._set_attributes(tile_set_node, tile)
745 for node in self._get_nodes(tile_set_node.childNodes, 'image'):
746 self._build_tile_set_tile_image(node, tile)
747 tile_set.tiles.append(tile)
749 def _build_tile_set_tile_image(self, tile_node, tile):
750 tile_image = TileImage()
751 self._set_attributes(tile_node, tile_image)
752 for node in self._get_nodes(tile_node.childNodes, 'data'):
753 self._set_attributes(node, tile_image)
754 tile_image.content = node.childNodes[0].nodeValue
755 tile.images.append(tile_image)
757 def _build_layer(self, layer_node, world_map):
758 layer = TileLayer()
759 self._set_attributes(layer_node, layer)
760 for node in self._get_nodes(layer_node.childNodes, 'data'):
761 self._set_attributes(node, layer)
762 if layer.encoding:
763 layer.encoded_content = node.lastChild.nodeValue
764 else:
765 #print 'has childnodes', node.hasChildNodes()
766 layer.encoded_content = []
767 for child in node.childNodes:
768 if child.nodeType == Node.ELEMENT_NODE and \
769 child.nodeName == "tile":
770 val = child.attributes["gid"].nodeValue
771 #print child, val
772 layer.encoded_content.append(val)
773 world_map.layers.append(layer)
775 def _build_world_map(self, world_node):
776 world_map = TileMap()
777 self._set_attributes(world_node, world_map)
778 if world_map.version != "1.0":
779 raise VersionError('this parser was made for maps of version 1.0, found version %s' % world_map.version)
780 for node in self._get_nodes(world_node.childNodes, 'tileset'):
781 self._build_tile_set(node, world_map)
782 for node in self._get_nodes(world_node.childNodes, 'layer'):
783 self._build_layer(node, world_map)
784 for node in self._get_nodes(world_node.childNodes, 'objectgroup'):
785 self._build_object_groups(node, world_map)
786 return world_map
788 def _build_object_groups(self, object_group_node, world_map):
789 object_group = MapObjectGroupLayer()
790 self._set_attributes(object_group_node, object_group)
791 for node in self._get_nodes(object_group_node.childNodes, 'object'):
792 tiled_object = MapObject()
793 self._set_attributes(node, tiled_object)
794 for img_node in self._get_nodes(node.childNodes, 'image'):
795 tiled_object.image_source = \
796 img_node.attributes['source'].nodeValue
797 object_group.objects.append(tiled_object)
798 # ISSUE 9
799 world_map.layers.append(object_group)
801 # -- helpers -- #
802 def _get_nodes(self, nodes, name):
803 for node in nodes:
804 if node.nodeType == Node.ELEMENT_NODE and node.nodeName == name:
805 yield node
807 def _set_attributes(self, node, obj):
808 attrs = node.attributes
809 for attr_name in list(attrs.keys()):
810 setattr(obj, attr_name, attrs.get(attr_name).nodeValue)
811 self._get_properties(node, obj)
813 def _get_properties(self, node, obj):
814 props = {}
815 for properties_node in self._get_nodes(node.childNodes, 'properties'):
816 for property_node in self._get_nodes(properties_node.childNodes, 'property'):
817 try:
818 props[property_node.attributes['name'].nodeValue] = \
819 property_node.attributes['value'].nodeValue
820 except KeyError:
821 props[property_node.attributes['name'].nodeValue] = \
822 property_node.lastChild.nodeValue
823 obj.properties.update(props)
826 # -- parsers -- #
827 def parse(self, file_name):
829 Parses the given map. Does no decoding nor loading of the data.
830 :return: instance of TileMap
832 # would be more elegant to use
833 # "with open(file_name, "rb") as tmx_file:" but that is python 2.6
834 self.map_file_name = os.path.abspath(file_name)
835 tmx_file = None
836 try:
837 tmx_file = open(self.map_file_name, "rb")
838 dom = minidom.parseString(tmx_file.read())
839 finally:
840 if tmx_file:
841 tmx_file.close()
842 for node in self._get_nodes(dom.childNodes, 'map'):
843 world_map = self._build_world_map(node)
844 break
845 world_map.map_file_name = self.map_file_name
846 world_map.convert()
847 return world_map
849 def parse_decode(self, file_name):
851 Parses the map but additionally decodes the data.
852 :return: instance of TileMap
854 world_map = self.parse(file_name)
855 world_map.decode()
856 return world_map
859 # -----------------------------------------------------------------------------
861 class AbstractResourceLoader(object):
863 Abstract base class for the resource loader.
867 FLIP_X = 1 << 31
868 FLIP_Y = 1 << 30
870 def __init__(self):
871 self.indexed_tiles = {} # {gid: (offsetx, offsety, image}
872 self.world_map = None
873 self._img_cache = {}
875 def _load_image(self, filename, colorkey=None): # -> image
877 Load a single image.
879 :Parameters:
880 filename : string
881 Path to the file to be loaded.
882 colorkey : tuple
883 The (r, g, b) color that should be used as colorkey
884 (or magic color).
885 Default: None
887 :rtype: image
890 raise NotImplementedError('This should be implemented in a inherited class')
892 def _load_image_file_like(self, file_like_obj, colorkey=None): # -> image
894 Load a image from a file like object.
896 :Parameters:
897 file_like_obj : file
898 This is the file like object to load the image from.
899 colorkey : tuple
900 The (r, g, b) color that should be used as colorkey
901 (or magic color).
902 Default: None
904 :rtype: image
906 raise NotImplementedError('This should be implemented in a inherited class')
908 def _load_image_parts(self, filename, margin, spacing, tilewidth, tileheight, colorkey=None): #-> [images]
910 Load different tile images from one source image.
912 :Parameters:
913 filename : string
914 Path to image to be loaded.
915 margin : int
916 The margin around the image.
917 spacing : int
918 The space between the tile images.
919 tilewidth : int
920 The width of a single tile.
921 tileheight : int
922 The height of a single tile.
923 colorkey : tuple
924 The (r, g, b) color that should be used as colorkey
925 (or magic color).
926 Default: None
928 Luckily that iteration is so easy in python::
931 w, h = image_size
932 for y in xrange(margin, h, tileheight + spacing):
933 for x in xrange(margin, w, tilewidth + spacing):
936 :rtype: a list of images
938 raise NotImplementedError('This should be implemented in a inherited class')
940 def load(self, tile_map):
943 self.world_map = tile_map
944 for tile_set in tile_map.tile_sets:
945 # do images first, because tiles could reference it
946 for img in tile_set.images:
947 if img.source:
948 self._load_image_from_source(tile_map, tile_set, img)
949 else:
950 tile_set.indexed_images[img.id] = self._load_tile_image(img)
951 # tiles
952 for tile in tile_set.tiles:
953 for img in tile.images:
954 if not img.content and not img.source:
955 # only image id set
956 indexed_img = tile_set.indexed_images[img.id]
957 self.indexed_tiles[int(tile_set.firstgid) + int(tile.id)] = (0, 0, indexed_img)
958 else:
959 if img.source:
960 self._load_image_from_source(tile_map, tile_set, img)
961 else:
962 indexed_img = self._load_tile_image(img)
963 self.indexed_tiles[int(tile_set.firstgid) + int(tile.id)] = (0, 0, indexed_img)
965 def _load_image_from_source(self, tile_map, tile_set, a_tile_image):
966 # relative path to file
967 img_path = os.path.join(os.path.dirname(tile_map.map_file_name), \
968 a_tile_image.source)
969 tile_width = int(tile_map.tilewidth)
970 tile_height = int(tile_map.tileheight)
971 if tile_set.tileheight:
972 tile_width = int(tile_set.tilewidth)
973 if tile_set.tilewidth:
974 tile_height = int(tile_set.tileheight)
975 offsetx = 0
976 offsety = 0
977 # the offset is used for pygame because the origin is topleft in pygame
978 if tile_height > tile_map.tileheight:
979 offsety = tile_height - tile_map.tileheight
980 idx = 0
981 for image in self._load_image_parts(img_path, \
982 tile_set.margin, tile_set.spacing, \
983 tile_width, tile_height, a_tile_image.trans):
984 self.indexed_tiles[int(tile_set.firstgid) + idx] = \
985 (offsetx, -offsety, image)
986 idx += 1
988 def _load_tile_image(self, a_tile_image):
989 img_str = a_tile_image.content
990 if a_tile_image.encoding:
991 if a_tile_image.encoding == 'base64':
992 img_str = decode_base64(a_tile_image.content)
993 else:
994 raise Exception('unknown image encoding %s' % a_tile_image.encoding)
995 sio = StringIO(img_str)
996 new_image = self._load_image_file_like(sio, a_tile_image.trans)
997 return new_image
999 # -----------------------------------------------------------------------------