304 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			304 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import collections
 | |
| import itertools
 | |
| import operator
 | |
| import heapq
 | |
| import copy
 | |
| from .pack_algo import PackingAlgorithm
 | |
| from .geometry import Point as P
 | |
| from .geometry import HSegment, Rectangle
 | |
| from .waste import WasteManager
 | |
| 
 | |
| 
 | |
| class Skyline(PackingAlgorithm):
 | |
|     """ Class implementing Skyline algorithm as described by
 | |
|     Jukka Jylanki - A Thousand Ways to Pack the Bin (February 27, 2010)
 | |
| 
 | |
|     _skyline:  stores all the segments at the top of the skyline.
 | |
|     _waste: Handles all wasted sections.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, width, height, rot=True, *args, **kwargs):
 | |
|         """
 | |
|         _skyline is the list used to store all the skyline segments, each 
 | |
|         one is a list with the format [x, y, width] where x is the x
 | |
|         coordinate of the left most point of the segment, y the y coordinate
 | |
|         of the segment, and width the length of the segment. The initial 
 | |
|         segment is allways [0, 0, surface_width]
 | |
|         
 | |
|         Arguments:
 | |
|             width (int, float): 
 | |
|             height (int, float):
 | |
|             rot (bool): Enable or disable rectangle rotation
 | |
|         """
 | |
|         self._waste_management = False
 | |
|         self._waste = WasteManager(rot=rot)
 | |
|         super(Skyline, self).__init__(width, height, rot, merge=False, *args, **kwargs)
 | |
| 
 | |
|     def _placement_points_generator(self, skyline, width):
 | |
|         """Returns a generator for the x coordinates of all the placement
 | |
|         points on the skyline for a given rectangle.
 | |
| 
 | |
|         WARNING: In some cases could be duplicated points, but it is faster
 | |
|         to compute them twice than to remove them.
 | |
|         
 | |
|         Arguments:
 | |
|             skyline (list): Skyline HSegment list
 | |
|             width (int, float): Rectangle width
 | |
| 
 | |
|         Returns:
 | |
|             generator
 | |
|         """ 
 | |
|         skyline_r = skyline[-1].right
 | |
|         skyline_l = skyline[0].left
 | |
| 
 | |
|         # Placements using skyline segment left point
 | |
|         ppointsl = (s.left for s in skyline if s.left+width <= skyline_r)
 | |
| 
 | |
|         # Placements using skyline segment right point
 | |
|         ppointsr = (s.right-width for s in skyline if s.right-width >= skyline_l)
 | |
| 
 | |
|         # Merge positions
 | |
|         return heapq.merge(ppointsl, ppointsr)
 | |
| 
 | |
|     def _generate_placements(self, width, height):
 | |
|         """
 | |
|         Generate a list with 
 | |
| 
 | |
|         Arguments:
 | |
|             skyline (list): SkylineHSegment list
 | |
|             width (number):
 | |
| 
 | |
|         Returns:
 | |
|             tuple (Rectangle, fitness):
 | |
|                 Rectangle: Rectangle in valid position
 | |
|                 left_skyline: Index for the skyline under the rectangle left edge.
 | |
|                 right_skyline: Index for the skyline under the rectangle right edte.
 | |
|         """
 | |
|         skyline = self._skyline
 | |
| 
 | |
|         points = collections.deque()
 | |
| 
 | |
|         left_index = right_index = 0 # Left and right side skyline index
 | |
|         support_height = skyline[0].top
 | |
|         support_index = 0 
 | |
|     
 | |
|         placements = self._placement_points_generator(skyline, width)
 | |
|         for p in placements:
 | |
| 
 | |
|             # If Rectangle's right side changed segment, find new support
 | |
|             if p+width > skyline[right_index].right:
 | |
|                 for right_index in range(right_index+1, len(skyline)):
 | |
|                     if skyline[right_index].top >= support_height:
 | |
|                         support_index = right_index
 | |
|                         support_height = skyline[right_index].top
 | |
|                     if p+width <= skyline[right_index].right:
 | |
|                         break
 | |
|                 
 | |
|             # If left side changed segment.
 | |
|             if p >= skyline[left_index].right:
 | |
|                 left_index +=1
 | |
|            
 | |
|             # Find new support if the previous one was shifted out.
 | |
|             if support_index < left_index:
 | |
|                 support_index = left_index
 | |
|                 support_height = skyline[left_index].top
 | |
|                 for i in range(left_index, right_index+1):
 | |
|                     if skyline[i].top >= support_height:
 | |
|                         support_index = i
 | |
|                         support_height = skyline[i].top
 | |
| 
 | |
|             # Add point if there is enought room at the top
 | |
|             if support_height+height <= self.height:
 | |
|                 points.append((Rectangle(p, support_height, width, height),\
 | |
|                     left_index, right_index))
 | |
| 
 | |
|         return points
 | |
| 
 | |
|     def _merge_skyline(self, skylineq, segment):
 | |
|         """
 | |
|         Arguments:
 | |
|             skylineq (collections.deque):
 | |
|             segment (HSegment):
 | |
|         """
 | |
|         if len(skylineq) == 0:
 | |
|             skylineq.append(segment)
 | |
|             return
 | |
| 
 | |
|         if skylineq[-1].top == segment.top:
 | |
|             s = skylineq[-1]
 | |
|             skylineq[-1] = HSegment(s.start, s.length+segment.length)
 | |
|         else:
 | |
|             skylineq.append(segment)
 | |
| 
 | |
|     def _add_skyline(self, rect):
 | |
|         """
 | |
|         Arguments:
 | |
|             seg (Rectangle):
 | |
|         """
 | |
|         skylineq = collections.deque([]) # Skyline after adding new one
 | |
|         
 | |
|         for sky in self._skyline:
 | |
|             if sky.right <= rect.left or sky.left >= rect.right:
 | |
|                 self._merge_skyline(skylineq, sky)
 | |
|                 continue
 | |
| 
 | |
|             if sky.left < rect.left and sky.right > rect.left:
 | |
|                 # Skyline section partially under segment left
 | |
|                 self._merge_skyline(skylineq, 
 | |
|                         HSegment(sky.start, rect.left-sky.left))
 | |
|                 sky = HSegment(P(rect.left, sky.top), sky.right-rect.left)
 | |
|             
 | |
|             if sky.left < rect.right:
 | |
|                 if sky.left == rect.left:
 | |
|                     self._merge_skyline(skylineq, 
 | |
|                         HSegment(P(rect.left, rect.top), rect.width))
 | |
|                 # Skyline section partially under segment right
 | |
|                 if sky.right > rect.right:
 | |
|                     self._merge_skyline(skylineq, 
 | |
|                         HSegment(P(rect.right, sky.top), sky.right-rect.right))
 | |
|                     sky = HSegment(sky.start, rect.right-sky.left)
 | |
|             
 | |
|             if sky.left >= rect.left and sky.right <= rect.right:
 | |
|                 # Skyline section fully under segment, account for wasted space
 | |
|                 if self._waste_management and sky.top < rect.bottom:
 | |
|                     self._waste.add_waste(sky.left, sky.top, 
 | |
|                         sky.length, rect.bottom - sky.top)
 | |
|             else:
 | |
|                 # Segment
 | |
|                 self._merge_skyline(skylineq, sky)
 | |
| 
 | |
|         # Aaaaand ..... Done
 | |
|         self._skyline = list(skylineq)
 | |
| 
 | |
|     def _rect_fitness(self, rect, left_index, right_index):
 | |
|         return rect.top
 | |
| 
 | |
|     def _select_position(self, width, height):
 | |
|         """
 | |
|         Search for the placement with the bes fitness for the rectangle.
 | |
| 
 | |
|         Returns:
 | |
|             tuple (Rectangle, fitness) - Rectangle placed in the fittest position
 | |
|             None - Rectangle couldn't be placed
 | |
|         """
 | |
|         positions = self._generate_placements(width, height)
 | |
|         if self.rot and width != height:
 | |
|             positions += self._generate_placements(height, width)
 | |
|         if not positions:
 | |
|             return None, None
 | |
|         return min(((p[0], self._rect_fitness(*p))for p in positions), 
 | |
|                 key=operator.itemgetter(1))
 | |
| 
 | |
|     def fitness(self, width, height):
 | |
|         """Search for the best fitness 
 | |
|         """
 | |
|         assert(width > 0 and height >0)
 | |
|         if width > max(self.width, self.height) or\
 | |
|             height > max(self.height, self.width):
 | |
|             return None
 | |
| 
 | |
|         # If there is room in wasted space, FREE PACKING!!
 | |
|         if self._waste_management:
 | |
|             if self._waste.fitness(width, height) is not None:
 | |
|                 return 0
 | |
| 
 | |
|         # Get best fitness segment, for normal rectangle, and for
 | |
|         # rotated rectangle if rotation is enabled.
 | |
|         rect, fitness = self._select_position(width, height)
 | |
|         return fitness
 | |
| 
 | |
|     def add_rect(self, width, height, rid=None):
 | |
|         """
 | |
|         Add new rectangle
 | |
|         """
 | |
|         assert(width > 0 and height > 0)
 | |
|         if width > max(self.width, self.height) or\
 | |
|             height > max(self.height, self.width):
 | |
|             return None
 | |
| 
 | |
|         rect = None
 | |
|         # If Waste managment is enabled, first try to place the rectangle there
 | |
|         if self._waste_management:
 | |
|             rect = self._waste.add_rect(width, height, rid)
 | |
| 
 | |
|         # Get best possible rectangle position
 | |
|         if not rect:
 | |
|             rect, _ = self._select_position(width, height)
 | |
|             if rect:
 | |
|                 self._add_skyline(rect)
 | |
| 
 | |
|         if rect is None:
 | |
|             return None
 | |
|         
 | |
|         # Store rectangle, and recalculate skyline
 | |
|         rect.rid = rid
 | |
|         self.rectangles.append(rect)
 | |
|         return rect
 | |
| 
 | |
|     def reset(self):
 | |
|         super(Skyline, self).reset()
 | |
|         self._skyline = [HSegment(P(0, 0), self.width)]
 | |
|         self._waste.reset()
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| class SkylineWMixin(Skyline):
 | |
|     """Waste managment mixin"""
 | |
|     def __init__(self, width, height, *args, **kwargs):
 | |
|         super(SkylineWMixin, self).__init__(width, height, *args, **kwargs)
 | |
|         self._waste_management = True
 | |
| 
 | |
| 
 | |
| class SkylineMwf(Skyline):
 | |
|     """Implements Min Waste fit heuristic, minimizing the area wasted under the
 | |
|     rectangle.
 | |
|     """
 | |
|     def _rect_fitness(self, rect, left_index, right_index):
 | |
|         waste = 0
 | |
|         for seg in self._skyline[left_index:right_index+1]:
 | |
|             waste +=\
 | |
|                 (min(rect.right, seg.right)-max(rect.left, seg.left)) *\
 | |
|                 (rect.bottom-seg.top)
 | |
| 
 | |
|         return waste
 | |
| 
 | |
|     def _rect_fitnes2s(self, rect, left_index, right_index):
 | |
|         waste = ((min(rect.right, seg.right)-max(rect.left, seg.left)) for seg in self._skyline[left_index:right_index+1])
 | |
|         return sum(waste)
 | |
| 
 | |
| class SkylineMwfl(Skyline):
 | |
|     """Implements Min Waste fit with low profile heuritic, minimizing the area
 | |
|     wasted below the rectangle, at the same time it tries to keep the height
 | |
|     minimal.
 | |
|     """ 
 | |
|     def _rect_fitness(self, rect, left_index, right_index):
 | |
|         waste = 0
 | |
|         for seg in self._skyline[left_index:right_index+1]:
 | |
|             waste +=\
 | |
|                 (min(rect.right, seg.right)-max(rect.left, seg.left)) *\
 | |
|                 (rect.bottom-seg.top)
 | |
| 
 | |
|         return waste*self.width*self.height+rect.top
 | |
| 
 | |
| 
 | |
| class SkylineBl(Skyline):
 | |
|     """Implements Bottom Left heuristic, the best fit option is that which
 | |
|     results in which the top side of the rectangle lies at the bottom-most 
 | |
|     position.
 | |
|     """
 | |
|     def _rect_fitness(self, rect, left_index, right_index):
 | |
|         return rect.top
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| class SkylineBlWm(SkylineBl, SkylineWMixin):
 | |
|     pass
 | |
| 
 | |
| class SkylineMwfWm(SkylineMwf, SkylineWMixin):
 | |
|     pass
 | |
| 
 | |
| class SkylineMwflWm(SkylineMwfl, SkylineWMixin):
 | |
|     pass
 |