pytmx/utils.py (view raw)
1from pygame import Rect
2from itertools import tee, islice, izip, product
3from collections import defaultdict
4from .constants import *
5
6
7def read_points(text):
8 return [ tuple(map(lambda x: int(x), i.split(',')))
9 for i in text.split() ]
10
11
12def parse_properties(node):
13 """
14 parse a node and return a dict that represents a tiled "property"
15 """
16
17 # the "properties" from tiled's tmx have an annoying quality that "name"
18 # and "value" is included. here we mangle it to get that junk out.
19
20 d = {}
21
22 for child in node.findall('properties'):
23 for subnode in child.findall('property'):
24 d[subnode.get('name')] = subnode.get('value')
25
26 return d
27
28
29def decode_gid(raw_gid):
30 # gids are encoded with extra information
31 # as of 0.7.0 it determines if the tile should be flipped when rendered
32 # as of 0.8.0 bit 30 determines if GID is rotated
33
34 flags = 0
35 if raw_gid & GID_TRANS_FLIPX == GID_TRANS_FLIPX: flags += TRANS_FLIPX
36 if raw_gid & GID_TRANS_FLIPY == GID_TRANS_FLIPY: flags += TRANS_FLIPY
37 if raw_gid & GID_TRANS_ROT == GID_TRANS_ROT: flags += TRANS_ROT
38 gid = raw_gid & ~(GID_TRANS_FLIPX | GID_TRANS_FLIPY | GID_TRANS_ROT)
39
40 return gid, flags
41
42
43def handle_bool(text):
44 # properly convert strings to a bool
45 try:
46 return bool(int(text))
47 except:
48 pass
49
50 try:
51 text = str(text).lower()
52 if text == "true": return True
53 if text == "yes": return True
54 if text == "false": return False
55 if text == "no": return False
56 except:
57 pass
58
59 raise ValueError
60
61
62# used to change the unicode string returned from xml to proper python
63# variable types.
64types = defaultdict(lambda: str)
65types.update({
66 "version": float,
67 "orientation": str,
68 "width": int,
69 "height": int,
70 "tilewidth": int,
71 "tileheight": int,
72 "firstgid": int,
73 "source": str,
74 "name": str,
75 "spacing": int,
76 "margin": int,
77 "trans": str,
78 "id": int,
79 "opacity": float,
80 "visible": handle_bool,
81 "encoding": str,
82 "compression": str,
83 "gid": int,
84 "type": str,
85 "x": int,
86 "y": int,
87 "value": str,
88})
89
90
91def pairwise(iterable):
92 # return a list as a sequence of pairs
93 a, b = tee(iterable)
94 next(b, None)
95 return izip(a, b)
96
97
98def group(l, n):
99 # return a list as a sequence of n tuples
100 return izip(*(islice(l, i, None, n) for i in xrange(n)))
101
102
103def buildDistributionRects(tmxmap, layer, tileset=None, real_gid=None):
104 """
105 generate a set of non-overlapping rects that represents the distribution
106 of the specified gid.
107
108 useful for generating rects for use in collision detection
109 """
110
111 if isinstance(tileset, int):
112 try:
113 tileset = tmxmap.tilesets[tileset]
114 except IndexError:
115 msg = "Tileset #{0} not found in map {1}."
116 raise IndexError, msg.format(tileset, tmxmap)
117
118 elif isinstance(tileset, str):
119 try:
120 tileset = [ t for t in tmxmap.tilesets if t.name == tileset ].pop()
121 except IndexError:
122 msg = "Tileset \"{0}\" not found in map {1}."
123 raise ValueError, msg.format(tileset, tmxmap)
124
125 elif tileset:
126 msg = "Tileset must be either a int or string. got: {0}"
127 raise ValueError, msg.format(type(tileset))
128
129 gid = None
130 if real_gid:
131 try:
132 gid, flags = tmxmap.map_gid(real_gid)[0]
133 except IndexError:
134 msg = "GID #{0} not found"
135 raise ValueError, msg.format(real_gid)
136
137 if isinstance(layer, int):
138 layer_data = tmxmap.getLayerData(layer).data
139 elif isinstance(layer, str):
140 try:
141 layer = [ l for l in tmxmap.tilelayers if l.name == layer ].pop()
142 layer_data = layer.data
143 except IndexError:
144 msg = "Layer \"{0}\" not found in map {1}."
145 raise ValueError, msg.format(layer, tmxmap)
146
147 p = product(xrange(tmxmap.width), xrange(tmxmap.height))
148 if gid:
149 points = [ (x,y) for (x,y) in p if layer_data[y][x] == gid ]
150 else:
151 points = [ (x,y) for (x,y) in p if layer_data[y][x] ]
152
153 rects = simplify(points, tmxmap.tilewidth, tmxmap.tileheight)
154 return rects
155
156
157def simplify(all_points, tilewidth, tileheight):
158 """
159 kludge:
160
161 "A kludge (or kluge) is a workaround, a quick-and-dirty solution,
162 a clumsy or inelegant, yet effective, solution to a problem, typically
163 using parts that are cobbled together."
164
165 -- wikipedia
166
167 turn a list of points into a rects
168 adjacent rects will be combined.
169
170 plain english:
171 the input list must be a list of tuples that represent
172 the areas to be combined into rects
173 the rects will be blended together over solid groups
174
175 so if data is something like:
176
177 0 1 1 1 0 0 0
178 0 1 1 0 0 0 0
179 0 0 0 0 0 4 0
180 0 0 0 0 0 4 0
181 0 0 0 0 0 0 0
182 0 0 1 1 1 1 1
183
184 you'll have the 4 rects that mask the area like this:
185
186 ..######......
187 ..####........
188 ..........##..
189 ..........##..
190 ..............
191 ....##########
192
193 pretty cool, right?
194
195 there may be cases where the number of rectangles is not as low as possible,
196 but I haven't found that it is excessively bad. certainly much better than
197 making a list of rects, one for each tile on the map!
198
199 """
200
201 def pick_rect(points, rects):
202 ox, oy = sorted([ (sum(p), p) for p in points ])[0][1]
203 x = ox
204 y = oy
205 ex = None
206
207 while 1:
208 x += 1
209 if not (x, y) in points:
210 if ex is None:
211 ex = x - 1
212
213 if ((ox, y+1) in points):
214 if x == ex + 1 :
215 y += 1
216 x = ox
217
218 else:
219 y -= 1
220 break
221 else:
222 if x <= ex: y-= 1
223 break
224
225 c_rect = Rect(ox*tilewidth,oy*tileheight,\
226 (ex-ox+1)*tilewidth,(y-oy+1)*tileheight)
227
228 rects.append(c_rect)
229
230 rect = Rect(ox,oy,ex-ox+1,y-oy+1)
231 kill = [ p for p in points if rect.collidepoint(p) ]
232 [ points.remove(i) for i in kill ]
233
234 if points:
235 pick_rect(points, rects)
236
237 rect_list = []
238 while all_points:
239 pick_rect(all_points, rect_list)
240
241 return rect_list
242