View Javadoc
1   /*
2    * The coLAB project
3    * Copyright (C) 2022-2023 AlbaSim, MEI, HEIG-VD, HES-SO
4    *
5    * Licensed under the MIT License
6    */
7   package ch.colabproject.colab.api.controller.card.grid;
8   
9   import java.util.ArrayList;
10  import java.util.Collection;
11  import java.util.HashMap;
12  import java.util.HashSet;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.Set;
16  
17  /**
18   * Utility class to help resolving cell positioning
19   *
20   * @author maxence
21   */
22  public final class Grid {
23  
24      /** Default X coordinate when a card is new on a grid */
25      public static int DEFAULT_X_COORDINATE = 1;
26  
27      /** Default Y coordinate when a card is new on a grid */
28      public static int DEFAULT_Y_COORDINATE = 1;
29  
30      /** Default width when a card is new on a grid */
31      public static int DEFAULT_WIDTH = 1;
32  
33      /** Default height when a card is new on a grid */
34      public static int DEFAULT_HEIGHT = 1;
35  
36      /** to store cells positions */
37      private Map<Integer, Map<Integer, GridCellWithId>> matrix = new HashMap<>();
38  
39      /** List of managed cells */
40      private Set<GridCellWithId> cells = new HashSet<>();
41  
42      /** left of the grid */
43      private Integer xMin = null;
44      /** top of the grid */
45      private Integer yMin = null;
46  
47      /** right of the grid */
48      private Integer xMax = null;
49      /** bottom of the grid */
50      private Integer yMax = null;
51  
52      /**
53       * Is the cell position free?
54       *
55       * @param x the x coordinate
56       * @param y the y coordinate
57       *
58       * @return true if the targeted cell is free
59       */
60      public boolean isFree(Integer x, Integer y) {
61          var column = matrix.get(x);
62          if (column != null) {
63              return column.get(y) == null;
64          }
65          return true;
66      }
67  
68      /**
69       * Get the cell at the position if any
70       *
71       * @param x the x coordinate
72       * @param y the y coordinate
73       *
74       * @return the cell ou null
75       */
76      public GridCellWithId getCellAt(Integer x, Integer y) {
77          var column = matrix.get(x);
78          if (column != null) {
79              GridCellWithId cell = column.get(y);
80              if (cell != null) {
81                  return cell;
82              }
83          }
84          return null;
85      }
86  
87      /**
88       * Make sure cell has a size and a position
89       *
90       * @param cell the cell to initialize
91       */
92      private void initCell(GridCellWithId cell) {
93          if (cell.getX() == null) {
94              cell.setX(DEFAULT_X_COORDINATE);
95          }
96  
97          if (cell.getY() == null) {
98              cell.setY(DEFAULT_Y_COORDINATE);
99          }
100         // make sure cell has a positive size
101         if (cell.getWidth() == null || cell.getWidth() < 1) {
102             cell.setWidth(DEFAULT_WIDTH);
103         }
104         if (cell.getHeight() == null || cell.getHeight() < 1) {
105             cell.setHeight(DEFAULT_HEIGHT);
106         }
107     }
108 
109     /**
110      * Reset the cell position and size
111      * @param cell the cell to change
112      */
113     public void resetCell(GridCell cell) {
114         cell.setX(DEFAULT_X_COORDINATE);
115         cell.setY(DEFAULT_Y_COORDINATE);
116         cell.setWidth(DEFAULT_WIDTH);
117         cell.setHeight(DEFAULT_HEIGHT);
118     }
119 
120     /**
121      * Is the cell position free?
122      *
123      * @param cell the cell to check
124      *
125      * @return true if the targeted cell is free
126      */
127     public boolean isFree(GridCellWithId cell) {
128         initCell(cell);
129         int cxMin = cell.getX();
130         int cxMax = cxMin + cell.getWidth() - 1;
131 
132         int cyMin = cell.getY();
133         int cyMax = cyMin + cell.getHeight() - 1;
134 
135         for (int x = cxMin; x <= cxMax; x++) {
136             var column = matrix.get(x);
137             if (column != null) {
138                 for (int y = cyMin; y <= cyMax; y++) {
139                     var c = column.get(y);
140                     if (c != null && c != cell) {
141                         return false;
142                     }
143                 }
144             }
145         }
146         return true;
147     }
148 
149     /**
150      * Make cell location within the matrix
151      *
152      * @param cell the cell to process
153      */
154     private void markCells(GridCellWithId cell) {
155         initCell(cell);
156         cells.add(cell);
157 
158         int cxMin = cell.getX();
159         int cxMax = cxMin + cell.getWidth() - 1;
160 
161         int cyMin = cell.getY();
162         int cyMax = cyMin + cell.getHeight() - 1;
163 
164         if (this.xMin == null) {
165             this.xMin = cxMin;
166         } else {
167             this.xMin = Math.min(this.xMin, cxMin);
168         }
169 
170         if (this.xMax == null) {
171             this.xMax = cxMax;
172         } else {
173             this.xMax = Math.max(this.xMax, cxMax);
174         }
175 
176         if (this.yMin == null) {
177             this.yMin = cyMin;
178         } else {
179             this.yMin = Math.min(this.yMin, cyMin);
180         }
181 
182         if (this.yMax == null) {
183             this.yMax = cyMax;
184         } else {
185             this.yMax = Math.max(this.yMax, cyMax);
186         }
187 
188         for (int x = cxMin; x <= cxMax; x++) {
189             var column = matrix.get(x);
190 
191             if (column == null) {
192                 column = new HashMap<>();
193                 matrix.put(x, column);
194             }
195 
196             for (int y = cyMin; y <= cyMax; y++) {
197                 column.put(y, cell);
198             }
199         }
200     }
201 
202     /**
203      * Last position, reading the from left to right, top to bottom
204      *
205      * @return the position of the last used coordinate or null if the grid is empty
206      */
207     private Coord findLastUsedPosition() {
208         for (int y = yMax; y >= yMin; y--) {
209             for (int x = this.xMax; x >= xMin; x--) {
210                 var column = matrix.get(x);
211                 if (column != null && column.get(y) != null) {
212                     return new Coord(x, y);
213                 }
214             }
215         }
216         return null;
217     }
218 
219     /**
220      * Find a new position for the given cell
221      *
222      * @param cell
223      */
224     private void findNewPosition(GridCellWithId cell) {
225         Coord lastCoord = findLastUsedPosition();
226         initCell(cell);
227 
228         if (lastCoord != null) {
229             // matrix is empty, use first position
230             cell.setX(DEFAULT_X_COORDINATE);
231             cell.setY(DEFAULT_Y_COORDINATE);
232         }
233         if (cell.getWidth() == 1 && cell.getHeight() == 1) {
234             if (lastCoord.x < xMax || xMax - xMin < 2) {
235                 // enough free space on current row
236                 cell.setX(lastCoord.x + 1);
237                 cell.setY(lastCoord.y);
238             } else {
239                 // end of line => append as first cell of a new row
240                 cell.setX(xMin);
241                 cell.setY(lastCoord.y + 1);
242             }
243         } else if (cell.getWidth() > cell.getHeight()) {
244             // wide or square cell
245             // add to bottom left
246             cell.setX(xMin);
247             cell.setY(yMax + 1);
248         } else {
249             // tall cell => add on top right
250             cell.setX(xMax + 1);
251             cell.setY(yMin);
252         }
253 
254     }
255 
256     /**
257      * Add a cell within the matrix. Fix any conflict
258      *
259      * @param cell the cell to add
260      */
261     public void addCell(GridCellWithId cell) {
262         if (isFree(cell)) {
263             markCells(cell);
264         } else {
265             findNewPosition(cell);
266             markCells(cell);
267         }
268     }
269 
270     /**
271      * Update all cell coordinates to have (xMin,yMin) = (1,1)
272      */
273     public void shift() {
274         // amount to add to each card
275         Integer shiftX = 1 - xMin;
276         Integer shiftY = 1 - yMin;
277 
278         cells.forEach(cell -> {
279             cell.setX(cell.getX() + shiftX);
280             cell.setY(cell.getY() + shiftY);
281         });
282 
283         xMin = 1;
284         yMin = 1;
285         xMax += shiftX;
286         yMax += shiftY;
287     }
288 
289     /**
290      * Process all cells and fix any conflicts
291      *
292      * @param cells the cells
293      *
294      * @return the grid
295      */
296     public static Grid resolveConflicts(Collection<? extends GridCellWithId> cells) {
297         ArrayList<GridCellWithId> list = new ArrayList<>();
298         list.addAll(cells);
299 
300         list.forEach(cell -> {
301             // make sure each cell has a size
302             if (cell.getWidth() == null) {
303                 cell.setWidth(1);
304             }
305             if (cell.getHeight() == null) {
306                 cell.setHeight(1);
307             }
308         });
309 
310         list.sort((a, b) -> {
311             boolean sameWidth = a.getWidth().equals(b.getWidth());
312             boolean sameHeight = a.getHeight().equals(b.getHeight());
313 
314             Integer aArea = a.getWidth() * a.getHeight();
315             Integer bArea = b.getWidth() * b.getHeight();
316 
317             if (sameHeight && sameWidth) {
318                 // same size: sort by id
319                 if (a.getId() < b.getId()) {
320                     return -1;
321                 } else if (a.getId() < b.getId()) {
322                     return 1;
323                 } else {
324                     return 0;
325                 }
326             } else if (sameWidth) {
327                 // same heigth, sort short to tall
328                 return a.getHeight() - b.getHeight();
329             } else if (sameHeight) {
330                 // same heigth, sort thin to thick
331                 return a.getWidth() - b.getWidth();
332             } else if (aArea.equals(bArea)) {
333                 // same area, different shape (eg 3x2 vs 2x3)
334                 // same heigth, sort short to tall
335                 return a.getHeight() - b.getHeight();
336             } else {
337                 // differant area, sort smaller first
338                 return aArea - bArea;
339             }
340         });
341 
342         List<GridCellWithId> conflictual = new ArrayList<>();
343         Grid grid = new Grid();
344 
345         list.forEach(cell -> {
346             if (grid.isFree(cell)) {
347                 grid.markCells(cell);
348             } else {
349                 conflictual.add(cell);
350             }
351         });
352         conflictual.forEach(cell -> {
353             grid.findNewPosition(cell);
354             grid.markCells(cell);
355         });
356         return grid;
357     }
358 
359     /**
360      * Simple class to depict a single coord
361      */
362     public static class Coord {
363 
364         /** x coord */
365         private Integer x;
366 
367         /** y coord */
368         private Integer y;
369 
370         /**
371          * simple constructor
372          *
373          * @param x x coordinate
374          * @param y y coordinate
375          */
376         public Coord(Integer x, Integer y) {
377             this.x = x;
378             this.y = y;
379         }
380 
381         @Override
382         public String toString() {
383             return "Coord{" + x + "," + y + '}';
384         }
385     }
386 }