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