tmxreader.py 33 KB

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