"""Map layers"""
from __future__ import division
import array
from tmxlib import helpers, tileset, tile, mapobject, image, fileio
[docs]class LayerList(helpers.NamedElementList):
"""A list of layers.
Allows indexing by name, and can only contain layers of a single map.
"""
def __init__(self, map, lst=None):
self.map = map
super(LayerList, self).__init__(lst)
[docs] def stored_value(self, layer):
"""Prevent layers that aren't from this map.
"""
if layer.map != self.map:
raise ValueError('Incompatible layer')
return layer
[docs]class Layer(object):
"""Base class for map layers
init agruments, which become attributes:
.. attribute:: map
The map this layer belongs to. Unlike tilesets, layers are tied to
a particular map and cannot be shared.
.. attribute:: name
Name of the layer
.. attribute:: visible
A boolean setting whether the layer is visible at all. (Actual
visibility also depends on `opacity`)
.. attribute:: opacity
Floating-point value for the visibility of the layer. (Actual
visibility also depends on `visible`)
Other attributes:
.. attribute:: properties
Dict of properties with string (or unicode) keys and values.
.. attribute:: type
``'tiles'`` if this is a tile layer, ``'objects'`` if it's an
object layer, ``'image'`` for an object layer.
.. attribute:: index
Index of this layer in the layer list
A Layer is false in a boolean context iff it is empty, that is, if all
tiles of a tile layer are false, or if an object layer contains no objects.
"""
def __init__(self, map, name, visible=True, opacity=1):
super(Layer, self).__init__()
self.map = map
self.name = name
self.visible = visible
self.opacity = opacity
self.properties = {}
@property
[docs] def index(self):
return self.map.layers.index(self)
def __repr__(self):
return '<%s #%s: %r at 0x%x>' % (type(self).__name__, self.index,
self.name, id(self))
[docs] def all_tiles(self):
"""Yield all tiles in this layer, including empty ones and tile objects
"""
return ()
[docs] def all_objects(self):
"""Yield all objects in this layer
"""
return ()
def __nonzero__(self):
raise NotImplementedError('Layer.__nonzero__ is virtual')
[docs] def to_dict(self):
"""Export to a dict compatible with Tiled's JSON plugin"""
d = dict(
name=self.name,
opacity=self.opacity,
visible=self.visible,
width=self.map.width,
height=self.map.height,
x=0,
y=0,
)
if self.properties:
d['properties'] = self.properties
return d
@classmethod
[docs] def from_dict(cls, dct, *args, **kwargs):
"""Import from a dict compatible with Tiled's JSON plugin"""
subclass = dict(
tilelayer=TileLayer,
objectgroup=ObjectLayer,
imagelayer=ImageLayer,
)[dct['type']]
return subclass.from_dict(dct, *args, **kwargs)
[docs]class TileLayer(Layer):
"""A tile layer
Acts as a 2D array of MapTile's, indexed by [x, y] coordinates.
Assignment is possible either via numeric values, or by assigning
a TilesetTile. In the latter case, if the tileset is not on the map yet,
it is added.
See :class:`Layer` documentation for most init arguments.
Other init agruments, which become attributes:
.. attribute:: data
Optional list (or array) containing the values of tiles in the
layer, as one long list in row-major order.
See :class:`TileLikeObject.value` for what the numbers will mean.
"""
def __init__(self, map, name, visible=True, opacity=1, data=None):
super(TileLayer, self).__init__(map=map, name=name,
visible=visible, opacity=opacity)
data_size = map.width * map.height
if data is None:
self.data = array.array('L', [0] * data_size)
else:
if len(data) != data_size:
raise ValueError('Invalid layer data size')
self.data = array.array('L', data)
self.encoding = 'base64'
self.compression = 'zlib'
self.type = 'tiles'
def _data_index(self, pos):
"""Get an index for the data array from (x, y) coordinates
"""
x, y = pos
if x < 0:
x += self.map.width
if y < 0:
y += self.map.height
return x + y * self.map.width
[docs] def __setitem__(self, pos, value):
"""Set the tile at the given position
The set value can be either an raw integer value, or a TilesetTile.
In the latter case, any tileset not in the map yet will be added
to it.
Supports negative indices by wrapping in the obvious way.
"""
if isinstance(value, tileset.TilesetTile):
try:
value = value.gid(self.map)
except helpers.TilesetNotInMapError:
# Add the tileset
self.map.tilesets.append(value.tileset)
value = value.gid(self.map)
elif value < 0 or (value & 0x0FFF) >= self.map.end_gid:
raise ValueError('GID not in map!')
self.data[self._data_index(pos)] = int(value)
[docs] def __getitem__(self, pos):
"""Get a MapTile representing the tile at the given position.
Supports negative indices by wrapping in the obvious way.
"""
return tile.MapTile(self, pos)
[docs] def all_tiles(self):
"""Yield all tiles in this layer, including empty ones.
"""
for y in range(self.map.height):
for x in range(self.map.width):
yield self[x, y]
[docs] def value_at(self, pos):
"""Return the value at the given position
See :class:`MapTile` for an explanation of the value.
"""
return self.data[self._data_index(pos)]
[docs] def set_value_at(self, pos, new):
"""Sets the raw value at the given position
See :class:`MapTile` for an explanation of the value.
"""
self.data[self._data_index(pos)] = new
def __nonzero__(self):
return any(self.all_tiles())
__bool__ = __nonzero__
[docs] def to_dict(self):
"""Export to a dict compatible with Tiled's JSON plugin"""
d = super(TileLayer, self).to_dict()
d.update(dict(
data=list(self.data),
type='tilelayer',
))
return d
@helpers.from_dict_method
[docs] def from_dict(cls, dct, map):
"""Import from a dict compatible with Tiled's JSON plugin"""
helpers.assert_item(dct, 'type', 'tilelayer')
helpers.assert_item(dct, 'width', map.width)
helpers.assert_item(dct, 'height', map.height)
helpers.assert_item(dct, 'x', 0)
helpers.assert_item(dct, 'y', 0)
self = cls(
map=map,
name=dct.pop('name'),
visible=dct.pop('visible', True),
opacity=dct.pop('opacity', 1),
data=dct.pop('data'),
)
self.properties.update(dct.pop('properties', {}))
return self
[docs]class ImageLayer(Layer):
"""An image layer
See :class:`Layer` documentation for most init arguments.
Other init agruments, which become attributes:
.. attribute:: image
The image to use for the layer
"""
type = 'image'
def __init__(self, map, name, visible=True, opacity=1, image=None):
super(ImageLayer, self).__init__(map=map, name=name,
visible=visible, opacity=opacity)
self.image = image
def __nonzero__(self):
"""An ImageLayer is "true" iff there's an image set on it."""
return bool(self.image)
__bool__ = __nonzero__
[docs] def to_dict(self):
"""Export to a dict compatible with Tiled's JSON plugin"""
d = super(ImageLayer, self).to_dict()
d.update(dict(
type='imagelayer',
image=self.image.source,
))
return d
@helpers.from_dict_method
[docs] def from_dict(cls, dct, map):
"""Import from a dict compatible with Tiled's JSON plugin"""
helpers.assert_item(dct, 'type', 'imagelayer')
helpers.assert_item(dct, 'width', map.width)
helpers.assert_item(dct, 'height', map.height)
helpers.assert_item(dct, 'x', 0)
helpers.assert_item(dct, 'y', 0)
self = cls(
map=map,
name=dct.pop('name'),
visible=dct.pop('visible', True),
opacity=dct.pop('opacity', 1),
image=image.open(dct.pop('image')),
)
self.properties.update(dct.pop('properties', {}))
return self
[docs]class ObjectLayer(Layer, helpers.NamedElementList):
"""A layer of objects.
Acts as a :class:`named list <tmxlib.helpers.NamedElementList>` of objects.
This means semantics similar to layer/tileset lists: indexing by name is
possible, where a name references the first object of such name.
See :class:`Layer` for inherited init arguments.
ObjectLayer-specific init arguments, which become attributes:
.. attribute:: color
The intended color of objects in this layer, as a triple of
floats (0..1)
"""
def __init__(self, map, name, visible=True, opacity=1, color=None):
super(ObjectLayer, self).__init__(map=map, name=name,
visible=visible, opacity=opacity)
self.type = 'objects'
self.color = color
[docs] def all_tiles(self):
"""Yield all tile objects in this layer, in order.
"""
for obj in self:
if obj.objtype == 'tile':
yield obj
[docs] def all_objects(self):
"""Yield all objects in this layer (i.e. return self)
"""
return self
[docs] def stored_value(self, item):
if item.layer is not self:
raise ValueError('Incompatible object')
return item
def __nonzero__(self):
return bool(len(self))
__bool__ = __nonzero__
[docs] def to_dict(self):
"""Export to a dict compatible with Tiled's JSON plugin"""
d = super(ObjectLayer, self).to_dict()
d.update(dict(
type='objectgroup',
objects=[o.to_dict() for o in self]
))
if self.color:
d['color'] = '#' + fileio.to_hexcolor(self.color)
return d
@helpers.from_dict_method
[docs] def from_dict(cls, dct, map):
"""Import from a dict compatible with Tiled's JSON plugin"""
helpers.assert_item(dct, 'type', 'objectgroup')
helpers.assert_item(dct, 'width', map.width)
helpers.assert_item(dct, 'height', map.height)
helpers.assert_item(dct, 'x', 0)
helpers.assert_item(dct, 'y', 0)
color = dct.pop('color', None)
if color:
color = fileio.from_hexcolor(color)
self = cls(
map=map,
name=dct.pop('name'),
visible=dct.pop('visible', True),
opacity=dct.pop('opacity', 1),
color=color,
)
self.properties.update(dct.pop('properties', {}))
for obj in dct.pop('objects', {}):
self.append(mapobject.MapObject.from_dict(obj, self))
return self