Esempio n. 1
0
class GridMapper(AbstractMapper):
    """
    Maps a 2-D data space to and from screen space by specifying a 2-tuple in
    data space or by specifying a pair of screen coordinates.

    The mapper concerns itself only with metric and not with orientation. So,
    to "flip" a screen space orientation, swap the appropriate screen space
    values for **x_low_pos**, **x_high_pos**, **y_low_pos**, and
    **y_high_pos**.
    """

    # The data-space bounds of the mapper.
    range = Instance(DataRange2D)

    # The screen space position of the lower bound of the horizontal axis.
    x_low_pos = Float(0.0)

    # The screen space position of the upper bound of the horizontal axis.
    x_high_pos = Float(1.0)

    # The screen space position of the lower bound of the vertical axis.
    y_low_pos = Float(0.0)

    # The screen space position of the upper bound of the vertical axis.
    y_high_pos = Float(1.0)

    # Convenience property for low and high positions in one structure.
    # Must be a tuple (x_low_pos, x_high_pos, y_low_pos, y_high_pos).
    screen_bounds = Property

    # Should the mapper stretch the dataspace when its screen space bounds are
    # modified (default), or should it preserve the screen-to-data ratio and
    # resize the data bounds?  If the latter, it will only try to preserve
    # the ratio if both screen and data space extents are non-zero.
    stretch_data_x = DelegatesTo("_xmapper", prefix="stretch_data")
    stretch_data_y = DelegatesTo("_ymapper", prefix="stretch_data")

    # Should the mapper try to maintain a fixed aspect ratio between x and y
    maintain_aspect_ratio = Bool

    # The aspect ratio that we wish to maintain
    aspect_ratio = Float(1.0)

    #------------------------------------------------------------------------
    # Private Traits
    #------------------------------------------------------------------------

    _updating_submappers = Bool(False)
    _updating_aspect = Bool(False)

    _xmapper = Instance(Base1DMapper)
    _ymapper = Instance(Base1DMapper)

    #------------------------------------------------------------------------
    # Public methods
    #------------------------------------------------------------------------

    def __init__(self, x_type="linear", y_type="linear", range=None, **kwargs):
        # TODO: This is currently an implicit assumption, i.e. that the range
        # will be passed in to the constructor.  It would be impossible to
        # create the xmapper and ymapper otherwise.  However, this should be
        # changed so that the mappers get created or modified in response to
        # the .range attribute changing, instead of requiring the range to
        # be passed in at construction time.
        self.range = range

        if "_xmapper" not in kwargs:
            if x_type == "linear":
                self._xmapper = LinearMapper(range=self.range.x_range)
            elif x_type == "log":
                self._xmapper = LogMapper(range=self.range.x_range)
            else:
                raise ValueError("Invalid x axis type: %s" % x_type)
        else:
            self._xmapper = kwargs.pop("_xmapper")

        if "_ymapper" not in kwargs:
            if y_type == "linear":
                self._ymapper = LinearMapper(range=self.range.y_range)
            elif y_type == "log":
                self._ymapper = LogMapper(range=self.range.y_range)
            else:
                raise ValueError("Invalid y axis type: %s" % y_type)
        else:
            self._ymapper = kwargs.pop("_ymapper")

        # Now that the mappers are created, we can go to the normal HasTraits
        # constructor, which might set values that depend on us having a valid
        # range and mappers.
        super(GridMapper, self).__init__(**kwargs)

    def map_screen(self, data_pts):
        """ map_screen(data_pts) -> screen_array

        Maps values from data space into screen space.
        """
        xs, ys = transpose(data_pts)
        screen_xs = self._xmapper.map_screen(xs)
        screen_ys = self._ymapper.map_screen(ys)
        screen_pts = column_stack([screen_xs, screen_ys])
        return screen_pts

    def map_data(self, screen_pts):
        """ map_data(screen_pts) -> data_vals

        Maps values from screen space into data space.
        """
        screen_xs, screen_ys = transpose(screen_pts)
        xs = self._xmapper.map_data(screen_xs)
        ys = self._ymapper.map_data(screen_ys)
        data_pts = column_stack([xs, ys])
        return data_pts

    def map_data_array(self, screen_pts):
        return self.map_data(screen_pts)

    #------------------------------------------------------------------------
    # Private Methods
    #------------------------------------------------------------------------

    def _update_bounds(self):
        with self._update_submappers():
            self._xmapper.screen_bounds = (self.x_low_pos, self.x_high_pos)
            self._ymapper.screen_bounds = (self.y_low_pos, self.y_high_pos)
        self.updated = True

    def _update_range(self):
        self.updated = True

    def _update_aspect_x(self):
        y_width = self._ymapper.high_pos - self._ymapper.low_pos
        if y_width == 0:
            return
        y_scale = (self._ymapper.range.high -
                   self._ymapper.range.low) / y_width
        x_range_low = self._xmapper.range.low
        x_width = self._xmapper.high_pos - self._xmapper.low_pos
        sign = self._xmapper.sign * self._ymapper.sign
        if x_width == 0 or sign == 0:
            return
        x_scale = sign * y_scale / self.aspect_ratio
        with self._update_aspect():
            self._xmapper.range.set_bounds(x_range_low,
                                           x_range_low + x_scale * x_width)

    def _update_aspect_y(self):
        x_width = self._xmapper.high_pos - self._xmapper.low_pos
        if x_width == 0:
            return
        x_scale = (self._xmapper.range.high -
                   self._xmapper.range.low) / x_width
        y_range_low = self._ymapper.range.low
        y_width = self._ymapper.high_pos - self._ymapper.low_pos
        sign = self._xmapper.sign * self._ymapper.sign
        if y_width == 0 or sign == 0:
            return
        y_scale = sign * x_scale * self.aspect_ratio
        with self._update_aspect():
            self._ymapper.range.set_bounds(y_range_low,
                                           y_range_low + y_scale * y_width)

    #------------------------------------------------------------------------
    # Property handlers
    #------------------------------------------------------------------------

    def _range_changed(self, old, new):
        if old is not None:
            old.on_trait_change(self._update_range, "updated", remove=True)
        if new is not None:
            new.on_trait_change(self._update_range, "updated")
            if self._xmapper is not None:
                self._xmapper.range = new.x_range
            if self._ymapper is not None:
                self._ymapper.range = new.y_range
            self._update_range()

    def _x_low_pos_changed(self):
        self._xmapper.low_pos = self.x_low_pos

    def _x_high_pos_changed(self):
        self._xmapper.high_pos = self.x_high_pos

    def _y_low_pos_changed(self):
        self._ymapper.low_pos = self.y_low_pos

    def _y_high_pos_changed(self):
        self._ymapper.high_pos = self.y_high_pos

    def _set_screen_bounds(self, new_bounds):
        # TODO: figure out a way to not need to do this check:
        if self.screen_bounds == new_bounds:
            return
        self.set(x_low_pos=new_bounds[0], trait_change_notify=False)
        self.set(x_high_pos=new_bounds[1], trait_change_notify=False)
        self.set(y_low_pos=new_bounds[2], trait_change_notify=False)
        self.set(y_high_pos=new_bounds[3], trait_change_notify=False)
        self._update_bounds()

    def _get_screen_bounds(self):
        return (self.x_low_pos, self.x_high_pos, self.y_low_pos,
                self.y_high_pos)

    def _updated_fired_for__xmapper(self):
        if not self._updating_aspect:
            if self.maintain_aspect_ratio and self.stretch_data_x:
                self._update_aspect_y()
        if not self._updating_submappers:
            self.updated = True

    def _updated_fired_for__ymapper(self):
        if not self._updating_aspect:
            if self.maintain_aspect_ratio and self.stretch_data_y:
                self._update_aspect_x()
        if not self._updating_submappers:
            self.updated = True

    @contextmanager
    def _update_submappers(self):
        self._updating_submappers = True
        try:
            yield
        finally:
            self._updating_submappers = False

    @contextmanager
    def _update_aspect(self):
        self._updating_aspect = True
        try:
            yield
        finally:
            self._updating_aspect = False
Esempio n. 2
0
class GridMapper(AbstractMapper):
    """
    Maps a 2-D data space to and from screen space by specifying a 2-tuple in
    data space or by specifying a pair of screen coordinates.

    The mapper concerns itself only with metric and not with orientation. So, to
    "flip" a screen space orientation, swap the appropriate screen space
    values for **x_low_pos**, **x_high_pos**, **y_low_pos**, and **y_high_pos**.
    """

    # The data-space bounds of the mapper.
    range = Instance(DataRange2D)

    # The screen space position of the lower bound of the horizontal axis.
    x_low_pos = Float(0.0)

    # The screen space position of the upper bound of the horizontal axis.
    x_high_pos  = Float(1.0)

    # The screen space position of the lower bound of the vertical axis.
    y_low_pos = Float(0.0)

    # The screen space position of the upper bound of the vertical axis.
    y_high_pos  = Float(1.0)

    # Convenience property for low and high positions in one structure.
    # Must be a tuple (x_low_pos, x_high_pos, y_low_pos, y_high_pos).
    screen_bounds = Property

    # Should the mapper stretch the dataspace when its screen space bounds are
    # modified (default), or should it preserve the screen-to-data ratio and
    # resize the data bounds?  If the latter, it will only try to preserve
    # the ratio if both screen and data space extents are non-zero.
    stretch_data_x = DelegatesTo("_xmapper", prefix="stretch_data")
    stretch_data_y = DelegatesTo("_ymapper", prefix="stretch_data")

    #------------------------------------------------------------------------
    # Private Traits
    #------------------------------------------------------------------------

    _updating_submappers = Bool(False)

    _xmapper = Instance(Base1DMapper)
    _ymapper = Instance(Base1DMapper)


    #------------------------------------------------------------------------
    # Public methods
    #------------------------------------------------------------------------

    def __init__(self, x_type="linear", y_type="linear", range=None, **kwargs):
        # TODO: This is currently an implicit assumption, i.e. that the range
        # will be passed in to the constructor.  It would be impossible to
        # create the xmapper and ymapper otherwise.  However, this should be
        # changed so that the mappers get created or modified in response to
        # the .range attribute changing, instead of requiring the range to
        # be passed in at construction time.
        self.range = range

        if "_xmapper" not in kwargs:
            if x_type == "linear":
                self._xmapper = LinearMapper(range=self.range.x_range)
            elif x_type == "log":
                self._xmapper = LogMapper(range=self.range.x_range)
            else:
                raise ValueError("Invalid x axis type: %s" % x_type)
        else:
            self._xmapper = kwargs.pop("_xmapper")

        if "_ymapper" not in kwargs:
            if y_type == "linear":
                self._ymapper = LinearMapper(range=self.range.y_range)
            elif y_type == "log":
                self._ymapper = LogMapper(range=self.range.y_range)
            else:
                raise ValueError("Invalid y axis type: %s" % y_type)
        else:
            self._ymapper = kwargs.pop("_ymapper")

        # Now that the mappers are created, we can go to the normal HasTraits
        # constructor, which might set values that depend on us having a valid
        # range and mappers.
        super(GridMapper, self).__init__(**kwargs)


    def map_screen(self, data_pts):
        """ map_screen(data_pts) -> screen_array

        Maps values from data space into screen space.
        """
        xs, ys = transpose(data_pts)
        screen_xs = self._xmapper.map_screen(xs)
        screen_ys = self._ymapper.map_screen(ys)
        return zip(screen_xs,screen_ys)

    def map_data(self, screen_pts):
        """ map_data(screen_pts) -> data_vals

        Maps values from screen space into data space.
        """
        screen_xs, screen_ys = transpose(screen_pts)
        xs = self._xmapper.map_data(screen_xs)
        ys = self._ymapper.map_data(screen_ys)
        return zip(xs,ys)

    def map_data_array(self, screen_pts):
        return self.map_data(screen_pts)


    #------------------------------------------------------------------------
    # Private Methods
    #------------------------------------------------------------------------

    def _update_bounds(self):
        self._updating_submappers = True
        self._xmapper.screen_bounds = (self.x_low_pos, self.x_high_pos)
        self._ymapper.screen_bounds = (self.y_low_pos, self.y_high_pos)
        self._updating_submappers = False
        self.updated = True

    def _update_range(self):
        self.updated = True


    #------------------------------------------------------------------------
    # Property handlers
    #------------------------------------------------------------------------

    def _range_changed(self, old, new):
        if old is not None:
            old.on_trait_change(self._update_range, "updated", remove=True)
        if new is not None:
            new.on_trait_change(self._update_range, "updated")
            if self._xmapper is not None:
                self._xmapper.range = new.x_range
            if self._ymapper is not None:
                self._ymapper.range = new.y_range
            self._update_range()

    def _x_low_pos_changed(self):
        self._xmapper.low_pos = self.x_low_pos

    def _x_high_pos_changed(self):
        self._xmapper.high_pos = self.x_high_pos

    def _y_low_pos_changed(self):
        self._ymapper.low_pos = self.y_low_pos

    def _y_high_pos_changed(self):
        self._ymapper.high_pos = self.y_high_pos

    def _set_screen_bounds(self, new_bounds):
        # TODO: figure out a way to not need to do this check:
        if self.screen_bounds == new_bounds:
            return
        self.set(x_low_pos = new_bounds[0], trait_change_notify=False)
        self.set(x_high_pos = new_bounds[1], trait_change_notify=False)
        self.set(y_low_pos = new_bounds[2], trait_change_notify=False)
        self.set(y_high_pos = new_bounds[3], trait_change_notify=False)
        self._update_bounds( )

    def _get_screen_bounds(self):
        return (self.x_low_pos, self.x_high_pos,
                self.y_low_pos, self.y_high_pos)

    def _updated_fired_for__xmapper(self):
        if not self._updating_submappers:
            self.updated = True

    def _updated_fired_for__ymapper(self):
        if not self._updating_submappers:
            self.updated = True