indexing description: "[ A Container of Drawables that sets up a totaly new coordinate system (`visible_area') for all contained drawables. Coordinates inside `visible_area' get projected into `bounding_box' of `Current' and everything outside this area is clipped. Like this all contained objects can easily be scrolled and/or zoomed. ]" date: "$Date$" revision: "$Revision$" class EM_ZOOMABLE_CONTAINER inherit EM_DRAWABLE_CONTAINER [EM_DRAWABLE] rename make as make_empty_container redefine width, height, draw, publish_mouse_event end DOUBLE_MATH export {NONE} all undefine is_equal, copy end create make feature -- Creation make (a_width, a_height: INTEGER) is -- Make empty container with size `a_width' and `a_height'. do make_empty_container create visible_area_changed_event width := a_width height := a_height -- Set up `visible_area' as a normal container (not zooming or scrolling yet). create visible_area.make_from_coordinates (0, 0, a_width, a_height) -- Per default set object area (where you can scroll to) to 100 times the visible area. create object_area.make_from_coordinates (0, 0, 10 * a_width, 10 * a_height) -- Initialize state. min_zoom_factor := min_zoom_factor_bound max_zoom_factor := max_zoom_factor_bound set_visible (True) ensure then no_contained_drawables: is_empty end feature -- Status report width: INTEGER -- Width of `Current' container. Everything outside won't be drawed. height: INTEGER -- Height of `Current' container. Everything outside won't be drawed. visible_area: EM_RECTANGLE -- Area defining coordinate system, -- coordinates inside this area will be stretched -- into area occupied by `Current' -- (`bounding_box') object_area: EM_RECTANGLE -- Area inside which all contained drawable objects -- should reside, scrolling is restricted to this area -- (scrolling commands ensure to not to scroll `center' -- of `visible_area' outside of `object_area') size_factor: DOUBLE is -- Factor by which areas will be scaled (quadratic zoom_factor) do Result := width * height / visible_area.size end zoom_factor: DOUBLE is -- Factor by which lengths will be scaled. do Result := sqrt (size_factor) end max_zoom_factor: DOUBLE -- Maximum zoom factor by which `Current' can be zoomed. min_zoom_factor: DOUBLE -- Minimum zoom factor by which `Current' can be zoomed. min_zoom_factor_bound: DOUBLE is -- Lowest possible value for `min_zoom_factor'. once Result := 1 / 1024 end max_zoom_factor_bound: DOUBLE is 1024.0 -- Highest possible value for `max_zoom_factor'. zoom_factor_precision: DOUBLE is -- Precision of `zoom_factor' -- (zoom factor's not differing more than this value are considered as equal). once Result := min_zoom_factor_bound / 10 end feature -- Status setting set_zoom_factor (a_zoom_factor: DOUBLE) is -- Zoom `Current' such as `zoom_factor' is `a_zoom_factor'. require zoom_factor_above_minimum: a_zoom_factor >= min_zoom_factor zoom_factor_below_maximum: a_zoom_factor <= max_zoom_factor do zoom (a_zoom_factor / zoom_factor) ensure -- TODO: This postcondition is buggy it (precision loss of double) -- zoom_factor_set: is_zoom_factor_equal (a_zoom_factor) end set_zoom_factor_range (a_min_zoom_factor, a_max_zoom_factor: DOUBLE) is -- Set `max_zoom_factor' to `a_max_zoom_factor' and -- `min_zoom_factor' to `a_min_zoom_factor'. -- Adopt current `zoom_factor' to be in range, if necessary. require min_less_than_max: a_min_zoom_factor <= a_max_zoom_factor a_min_zoom_factor_is_valid_minimum: a_min_zoom_factor >= min_zoom_factor_bound a_max_zoom_factor_is_valid_maximum: a_max_zoom_factor <= max_zoom_factor_bound do if a_min_zoom_factor >= max_zoom_factor then set_max_zoom_factor (a_max_zoom_factor) set_min_zoom_factor (a_min_zoom_factor) else set_min_zoom_factor (a_min_zoom_factor) set_max_zoom_factor (a_max_zoom_factor) end ensure min_zoom_factor_set: min_zoom_factor = a_min_zoom_factor max_zoom_factor_set: max_zoom_factor = a_max_zoom_factor end set_max_zoom_factor (a_zoom_factor: DOUBLE) is -- Set `max_zoom_factor' to `a_zoom_factor'. -- Adopt current `zoom_factor' to be in range, if necessary. require a_zoom_factor_is_valid_maximum: a_zoom_factor <= max_zoom_factor_bound a_zoom_factor_above_minimum: a_zoom_factor >= min_zoom_factor do max_zoom_factor := a_zoom_factor if zoom_factor > max_zoom_factor then -- Need to adopt current zoom. set_zoom_factor (max_zoom_factor) end ensure max_zoom_factor_set: max_zoom_factor = a_zoom_factor end set_min_zoom_factor (a_zoom_factor: DOUBLE) is -- Set `min_zoom_factor' to `a_zoom_factor'. -- Adopt current `zoom_factor' to be in range, if necessary. require a_zoom_factor_is_valid_maximum: a_zoom_factor <= max_zoom_factor_bound a_zoom_factor_above_minimum: a_zoom_factor >= min_zoom_factor do min_zoom_factor := a_zoom_factor if zoom_factor < min_zoom_factor then -- Need to adopt current zoom. set_zoom_factor (min_zoom_factor) end ensure min_zoom_factor_set: min_zoom_factor = a_zoom_factor end set_min_zoom_factor_to_fit_object_area is -- Set `min_zoom_factor' such that it is not possible to zoom -- farther than until `object_area' fits exactly into `Current'. local new_minimum: DOUBLE do new_minimum := (width / object_area.width).min (height / object_area.height) if new_minimum > max_zoom_factor then set_max_zoom_factor (new_minimum) end set_min_zoom_factor (new_minimum) ensure min_zoom_factor_set_to_fit_object_area: min_zoom_factor = (width / object_area.width).min (height / object_area.height) end set_object_area (an_area: EM_RECTANGLE) is -- Set `object_area' to `an_area'. require an_area_not_void: an_area /= Void do object_area := an_area ensure object_area_set: object_area = an_area end calculate_object_area is -- Calculate `object_area' from all contained objects -- to tightly surround them. local cursor: DS_LINKED_LIST_CURSOR [EM_DRAWABLE] bbox: EM_RECTANGLE do if not is_empty then from cursor := new_cursor cursor.start object_area := cursor.item.bounding_box.twin until cursor.after loop bbox := cursor.item.bounding_box object_area.extend (bbox.upper_left) object_area.extend (bbox.lower_right) cursor.forth end else -- Otherwise set default object area (much too big). create object_area.make_from_coordinates (0, 0, 10 * width, 10 * height) end end set_size (a_width, a_height: INTEGER) is -- Set size of `Current' to `a_width' and `a_height'. -- Adopt `visible_area' accordingly. require a_width_positive: a_width > 0 a_height_positive: a_height > 0 local va_width, va_height: DOUBLE do -- Adopt `visible_area'. va_width := visible_area.width / width * a_width va_height := visible_area.height / height * a_height visible_area.set_size (va_width, va_height) -- Set size. width := a_width height := a_height ensure width_set: width = a_width height_set: height = a_height end feature -- Commands zoom (a_zoom_factor: DOUBLE) is -- Zoom `Current' by `a_zoom_factor'. require a_zoom_factor_is_positive: a_zoom_factor > 0 do if zoom_factor * a_zoom_factor > max_zoom_factor then visible_area.zoom (zoom_factor / max_zoom_factor) elseif zoom_factor * a_zoom_factor < min_zoom_factor then visible_area.zoom (zoom_factor / min_zoom_factor) else visible_area.zoom (1 / a_zoom_factor) end visible_area_changed_event.publish ([visible_area]) ensure not_zoomed_over_zoom_max: zoom_factor <= max_zoom_factor + zoom_factor_precision not_zoomed_over_zoom_min: zoom_factor >= min_zoom_factor - zoom_factor_precision end scroll (a_direction: EM_VECTOR_2D) is -- Scroll `Current' by `a_direction' -- in object coordinates. require a_direction_not_void: a_direction /= Void do -- Move visible area by `a_direction'. visible_area.move_by (a_direction) -- Ensure not to scroll out of object_area. -- Keep center of `visible_area' inside `object_area'. if visible_area.center.x > object_area.right_bound then visible_area.left_by (visible_area.center.x - object_area.right_bound) end if visible_area.center.x < object_area.left_bound then visible_area.right_by (object_area.left_bound - visible_area.center.x) end if visible_area.center.y > object_area.lower_bound then visible_area.up_by (visible_area.center.y - object_area.lower_bound) end if visible_area.center.y < object_area.upper_bound then visible_area.down_by (object_area.upper_bound - visible_area.center.y) end -- Ensure not to zoom out of bound. zoom (1.0) end scroll_proportional (a_direction: EM_VECTOR_2D) is -- Scroll `Current' by `a_direction' -- in transformed coordinates require a_direction_not_void: a_direction /= Void local transformed_direction: EM_VECTOR_2D zoom_factor_x, zoom_factor_y: DOUBLE do -- Transform direction, such that when visible area gets moved by it -- it will perform the move of `direction' in projection area. zoom_factor_x := width / visible_area.width zoom_factor_y := height / visible_area.height create transformed_direction.make (a_direction.x / zoom_factor_x, a_direction.y / zoom_factor_y) -- Move visible area by `transformed_direction'. visible_area.move_by (transformed_direction) -- Ensure not to scroll out of object_area. -- Keep center of `visible_area' inside `object_area'. if visible_area.center.x > object_area.right_bound then visible_area.left_by (visible_area.center.x - object_area.right_bound) end if visible_area.center.x < object_area.left_bound then visible_area.right_by (object_area.left_bound - visible_area.center.x) end if visible_area.center.y > object_area.lower_bound then visible_area.up_by (visible_area.center.y - object_area.lower_bound) end if visible_area.center.y < object_area.upper_bound then visible_area.down_by (object_area.upper_bound - visible_area.center.y) end -- Ensure not to zoom out of bound. zoom (1.0) end center_on (a_position: EM_VECTOR_2D) is -- Scroll to have `a_position' in the center of `Current'. require a_position_not_void: a_position /= Void do scroll (a_position - visible_area.center) end scroll_and_zoom_to_rectangle (an_area: EM_RECTANGLE) is -- Zoom and scroll to fit `an_area' into `visible_area' (centered) require an_area_not_void: an_area /= Void local zf: DOUBLE do -- Center on `an_area' center_on (an_area.center) -- Calculate zoom factor to fit area into `visible_area'. zf := width / an_area.width zf := zf.min (height / an_area.height) -- Ensure not to zoom out of zoom factor bounds and zoom. zf := zf.max (min_zoom_factor) zf := zf.min (max_zoom_factor) set_zoom_factor (zf) end feature -- Queries is_zoom_factor_equal (a_zoom_factor: DOUBLE): BOOLEAN is -- Check if `zoom_factor' is considered equal to `a_zoom_factor'. -- Zoom factors are considered as equal, if they don't differ by more than `zoom_factor_precision'. do Result := (zoom_factor - a_zoom_factor).abs <= zoom_factor_precision end feature -- Drawing draw (a_surface: EM_SURFACE) is -- Draw `Current' to `a_surface' local old_clipping: EM_RECTANGLE old_coords: EM_RECTANGLE visible_part: EM_RECTANGLE visible_objects_part: EM_RECTANGLE cursor: DS_LINEAR_CURSOR [EM_DRAWABLE] translation: EM_VECTOR_2D do if is_visible then -- Set up coordinate system to clip coordinates to `Current's bounding box. old_clipping := a_surface.coordinate_area visible_part := old_clipping.intersection (bounding_box) a_surface.clip_coordinates (visible_part) -- Translate coordinate system for drawing all contained objects and `background'. create translation.make (x, y) a_surface.translate_coordinates (translation) -- Set up coordinate system to draw `visible_area' into `bounding_box'. old_coords := a_surface.coordinate_area create visible_objects_part.make (object_point (old_coords.upper_left.x, old_coords.upper_left.y), object_point (old_coords.lower_right.x, old_coords.lower_right.y)) a_surface.transform_coordinates (visible_objects_part) -- Draw all contained objects. cursor := new_cursor from cursor.start until cursor.off loop a_surface.draw_object (cursor.item) cursor.forth end -- Reset coordinate system. a_surface.transform_coordinates (old_coords) a_surface.translate_coordinates (- translation) a_surface.clip_coordinates (old_clipping) end end feature -- Zoom and Scroll Events visible_area_changed_event: EM_EVENT_CHANNEL [TUPLE []] -- Event when visible area has been changed (either zoomed or scrolled) feature -- Mouse Events publish_mouse_event (a_mouse_event: EM_MOUSE_EVENT) is -- Publish mouse event to children -- when `a_mouse_event' occured on `Current' -- (pass object coordinates to children). local cursor: DS_BILINKED_LIST_CURSOR [EM_DRAWABLE] transformed_mouse_event: EM_MOUSE_EVENT translation, transformed_point: EM_VECTOR_2D do if bounding_box.has (a_mouse_event.proportional_position) then -- Transform `a_mouse_event' into `Current' coordinate system. transformed_mouse_event := a_mouse_event. twin transformed_mouse_event.set_caught (False) create translation.make (x, y) transformed_point := a_mouse_event.proportional_position - translation transformed_point := object_point (transformed_point.x, transformed_point.y) transformed_mouse_event.set_proportional_position (transformed_point) -- First publish mouse events of `Current'. dispatch_mouse_event (transformed_mouse_event) -- Publish `transformed_mouse_event' to all children. -- (in top to bottom order until `caught') cursor := new_cursor from cursor.finish until cursor.before or transformed_mouse_event.caught loop cursor.item.publish_mouse_event (transformed_mouse_event) cursor.back end -- Publish mouse on item event with drawable object that caught the event. if transformed_mouse_event.caught then cursor.forth dispatch_mouse_on_item_event (cursor.item, transformed_mouse_event) -- Propagate to caller if event was caught. a_mouse_event.set_caught (True) end end end feature -- Local Coordinate Transformation transformed_object_point (a_point: EM_VECTOR_2D): EM_VECTOR_2D is -- Coordinates where `a_point' in object space is transformed to by `Current' (zoomed and/or scrolled). require a_point_not_void: a_point /= Void local px, py: DOUBLE do px := (a_point.x - visible_area.left_bound) * width / visible_area.width py := (a_point.y - visible_area.upper_bound) * height / visible_area.height create Result.make (px, py) ensure result_created: Result /= Void end object_point (a_x, a_y: DOUBLE): EM_VECTOR_2D is -- Point coordinate in object space that corresponds to position `a_x' and `a_y' on `Current'. local px, py: DOUBLE do px := (a_x * visible_area.width / width) + visible_area.left_bound py := (a_y * visible_area.height / height) + visible_area.upper_bound create Result.make (px, py) ensure result_created: Result /= Void end invariant min_zoom_factor_above_minimum_bound: min_zoom_factor >= min_zoom_factor_bound max_zoom_factor_below_maximum_bound: max_zoom_factor <= max_zoom_factor_bound min_zoom_factor_below_maximum_zoom_factor: min_zoom_factor <= max_zoom_factor zoom_factor_above_minimum: zoom_factor >= min_zoom_factor - zoom_factor_precision zoom_factor_below_maximum: zoom_factor <= max_zoom_factor + zoom_factor_precision end