Source: src/layouts/JustifiedLayout.ts

  1. import { find_path } from "./lib/dijkstra";
  2. import { getStyleNames, assignOptions, cloneItems, isObject, getRangeCost } from "../utils";
  3. import {
  4. ILayout,
  5. IRectlProperties,
  6. SizeType,
  7. IInfiniteGridItem,
  8. IInfiniteGridGroup,
  9. } from "../types";
  10. interface Link {
  11. path: number[];
  12. cost: number;
  13. length: number;
  14. currentNode: number;
  15. isOver?: boolean;
  16. }
  17. /**
  18. * @classdesc 'justified' is a printing term with the meaning that 'it fits in one row wide'. JustifiedLayout is a layout that the card is filled up on the basis of a line given a size.
  19. * @ko 'justified'는 '1행의 너비에 맞게 꼭 들어찬'이라는 의미를 가진 인쇄 용어다. 용어의 의미대로 너비가 주어진 사이즈를 기준으로 카드가 가득 차도록 배치하는 레이아웃이다.
  20. * @class eg.InfiniteGrid.JustifiedLayout
  21. * @param {Object} [options] The option object of eg.InfiniteGrid.JustifiedLayout module <ko>eg.InfiniteGrid.JustifiedLayout 모듈의 옵션 객체</ko>
  22. * @param {String} [options.margin=0] Margin used to create space around items <ko>아이템들 사이의 공간</ko>
  23. * @param {Boolean} [options.horizontal=false] Direction of the scroll movement (false: vertical, true: horizontal) <ko>스크롤 이동 방향 (false: 세로방향, true: 가로방향)</ko>
  24. * @param {Number} [options.minSize=0] Minimum size of item to be resized <ko> 아이템이 조정되는 최소 크기 </ko>
  25. * @param {Number} [options.maxSize=0] Maximum size of item to be resized <ko> 아이템이 조정되는 최대 크기 </ko>
  26. * @param {Array|Number} [options.column=[1, 8]] The number of items in a line <ko> 한 줄에 들어가는 아이템의 개수 </ko>
  27. * @param {Array|Number} [options.row=0] The number or range of rows in a group, 0 is not set <ko>한 그룹에 들어가는 열의 개수, 0은 미설정이다</ko>
  28. * @example
  29. ```
  30. <script>
  31. var ig = new eg.InfiniteGrid("#grid". {
  32. horizontal: true,
  33. });
  34. ig.setLayout(eg.InfiniteGrid.JustifiedLayout, {
  35. margin: 10,
  36. minSize: 100,
  37. maxSize: 300,
  38. });
  39. // or
  40. var layout = new eg.InfiniteGrid.JustifiedLayout({
  41. margin: 10,
  42. minSize: 100,
  43. maxSize: 300,
  44. column: 5,
  45. horizontal: true,
  46. });
  47. </script>
  48. ```
  49. **/
  50. class JustifiedLayout implements ILayout {
  51. public options: {
  52. margin: number;
  53. minSize: number;
  54. maxSize: number;
  55. column: number | number[];
  56. horizontal: boolean;
  57. row: number | number[];
  58. };
  59. private _style: IRectlProperties;
  60. private _size: number;
  61. constructor(options: Partial<JustifiedLayout["options"]> = {}) {
  62. this.options = assignOptions(
  63. {
  64. margin: 0,
  65. horizontal: false,
  66. minSize: 0,
  67. maxSize: 0,
  68. column: [1, 8],
  69. row: 0,
  70. },
  71. options
  72. );
  73. this._style = getStyleNames(this.options.horizontal);
  74. this._size = 0;
  75. }
  76. /**
  77. * Set the viewport size of the layout.
  78. * @ko 레이아웃의 가시 사이즈를 설정한다.
  79. * @method eg.InfiniteGrid.JustifiedLayout#setSize
  80. * @param {Number} size The viewport size of container area where items are added to a layout <ko>레이아웃에 아이템을 추가하는 컨테이너 영역의 가시 사이즈</ko>
  81. * @return {eg.InfiniteGrid.JustifiedLayout} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
  82. * @example
  83. * layout.setSize(800);
  84. */
  85. public setSize(size: number) {
  86. this._size = size;
  87. return this;
  88. }
  89. /**
  90. * Adds items at the bottom of a outline.
  91. * @ko 아이템들을 아웃라인 아래에 추가한다.
  92. * @method eg.InfiniteGrid.JustifiedLayout#append
  93. * @param {Array} items Array of items to be layouted <ko>레이아웃할 아이템들의 배열</ko>
  94. * @param {Array} [outline=[]] Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
  95. * @return {Object} Layouted items and outline of start and end <ko> 레이아웃이 된 아이템과 시작과 끝의 아웃라인이 담긴 정보</ko>
  96. * @example
  97. * layout.prepend(items, [100]);
  98. */
  99. public append(items: IInfiniteGridItem[], outline?: number[], cache?: boolean) {
  100. return this._insert(items, outline, true, cache);
  101. }
  102. /**
  103. * Adds items at the top of a outline.
  104. * @ko 아이템을 아웃라인 위에 추가한다.
  105. * @method eg.InfiniteGrid.JustifiedLayout#prepend
  106. * @param {Array} items Array of items to be layouted <ko>레이아웃할 아이템들의 배열</ko>
  107. * @param {Array} [outline=[]] Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
  108. * @return {Object} Layouted items and outline of start and end <ko> 레이아웃이 된 아이템과 시작과 끝의 아웃라인이 담긴 정보</ko>
  109. * @example
  110. * layout.prepend(items, [100]);
  111. */
  112. public prepend(items: IInfiniteGridItem[], outline?: number[], cache?: boolean) {
  113. return this._insert(items, outline, false, cache);
  114. }
  115. /**
  116. * Adds items of groups at the bottom of a outline.
  117. * @ko 그룹들의 아이템들을 아웃라인 아래에 추가한다.
  118. * @method eg.InfiniteGrid.JustifiedLayout#layout
  119. * @param {Array} groups Array of groups to be layouted <ko>레이아웃할 그룹들의 배열</ko>
  120. * @param {Array} outline Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
  121. * @return {eg.InfiniteGrid.JustifiedLayout} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
  122. * @example
  123. * layout.layout(groups, [100]);
  124. */
  125. public layout(groups: IInfiniteGridGroup[] = [], outline: number[] = []) {
  126. const length = groups.length;
  127. let point = outline;
  128. for (let i = 0; i < length; ++i) {
  129. const group = groups[i];
  130. const outlines = this._layout(group.items, point, true);
  131. group.outlines = outlines;
  132. point = outlines.end;
  133. }
  134. return this;
  135. }
  136. private _layout(items: IInfiniteGridItem[], outline: number[], isAppend?: boolean) {
  137. const row = this.options.row;
  138. let path: string[] = [];
  139. if (items.length) {
  140. path = row ? this._getRowPath(items) : this._getPath(items);
  141. }
  142. return this._setStyle(items, path, outline, isAppend);
  143. }
  144. private _getPath(items: IInfiniteGridItem[]) {
  145. const lastNode = items.length;
  146. const column = this.options.column;
  147. const [minColumn, maxColumn]: number[] = isObject(column)
  148. ? column
  149. : [column, column];
  150. const graph = (nodeKey: string) => {
  151. const results: { [key: string]: number } = {};
  152. const currentNode = parseInt(nodeKey, 10);
  153. for (let nextNode = Math.min(currentNode + minColumn, lastNode); nextNode <= lastNode; ++nextNode) {
  154. if (nextNode - currentNode > maxColumn) {
  155. break;
  156. }
  157. let cost = this._getCost(
  158. items,
  159. currentNode,
  160. nextNode,
  161. );
  162. if (cost < 0 && nextNode === lastNode) {
  163. cost = 0;
  164. }
  165. results[`${nextNode}`] = Math.pow(cost, 2);
  166. }
  167. return results;
  168. };
  169. // shortest path for items' total height.
  170. return find_path(graph, "0", `${lastNode}`);
  171. }
  172. private _getRowPath(items: IInfiniteGridItem[]) {
  173. const column = this.options.column;
  174. const row = this.options.row;
  175. const columnRange = isObject(column) ? column : [column, column];
  176. const rowRange: number[] = isObject(row) ? row : [row, row];
  177. const pathLink = this._getRowLink(items, {
  178. path: [0],
  179. cost: 0,
  180. length: 0,
  181. currentNode: 0,
  182. }, columnRange, rowRange);
  183. return pathLink?.path.map((node) => `${node}`) ?? [];
  184. }
  185. private _getRowLink(
  186. items: IInfiniteGridItem[],
  187. currentLink: Link,
  188. columnRange: number[],
  189. rowRange: number[]
  190. ): Link {
  191. const [minColumn] = columnRange;
  192. const [minRow, maxRow] = rowRange;
  193. const lastNode = items.length;
  194. const {
  195. path,
  196. length: pathLength,
  197. cost,
  198. currentNode
  199. } = currentLink;
  200. // not reached lastNode but path is exceed or the number of remaining nodes is less than minColumn.
  201. if (currentNode < lastNode && (maxRow <= pathLength || currentNode + minColumn > lastNode)) {
  202. const rangeCost = getRangeCost(lastNode - currentNode, columnRange);
  203. const lastCost = rangeCost * Math.abs(this._getCost(items, currentNode, lastNode));
  204. return {
  205. ...currentLink,
  206. length: pathLength + 1,
  207. path: [...path, lastNode],
  208. currentNode: lastNode,
  209. cost: cost + lastCost,
  210. isOver: true,
  211. };
  212. } else if (currentNode >= lastNode) {
  213. return {
  214. ...currentLink,
  215. currentNode: lastNode,
  216. isOver: minRow > pathLength || maxRow < pathLength,
  217. };
  218. } else {
  219. return this._searchRowLink(items, currentLink, lastNode, columnRange, rowRange);
  220. }
  221. }
  222. private _searchRowLink(
  223. items: IInfiniteGridItem[],
  224. currentLink: Link,
  225. lastNode: number,
  226. columnRange: number[],
  227. rowRange: number[]
  228. ) {
  229. const [minColumn, maxColumn] = columnRange;
  230. const {
  231. currentNode,
  232. path,
  233. length: pathLength,
  234. cost
  235. } = currentLink;
  236. const length = Math.min(lastNode, currentNode + maxColumn);
  237. const links: Link[] = [];
  238. for (let nextNode = currentNode + minColumn; nextNode <= length; ++nextNode) {
  239. if (nextNode === currentNode) {
  240. continue;
  241. }
  242. const nextCost = Math.abs(this._getCost(items, currentNode, nextNode));
  243. const nextLink = this._getRowLink(items, {
  244. path: [...path, nextNode],
  245. length: pathLength + 1,
  246. cost: cost + nextCost,
  247. currentNode: nextNode,
  248. }, columnRange, rowRange);
  249. if (nextLink) {
  250. links.push(nextLink);
  251. }
  252. }
  253. links.sort((a, b) => {
  254. const aIsOver = a.isOver;
  255. const bIsOver = b.isOver;
  256. if (aIsOver !== bIsOver) {
  257. // If it is over, the cost is high.
  258. return aIsOver ? 1 : -1;
  259. }
  260. const aRangeCost = getRangeCost(a.length, rowRange);
  261. const bRangeCost = getRangeCost(b.length, rowRange);
  262. return aRangeCost - bRangeCost || a.cost - b.cost;
  263. });
  264. // It returns the lowest cost link.
  265. return links[0];
  266. }
  267. private _getSize(items: IInfiniteGridItem[], size1Name: SizeType, size2Name: SizeType) {
  268. const margin = this.options.margin;
  269. const size = items.reduce((sum, item) => sum +
  270. (item.orgSize![size2Name]) / item.orgSize![size1Name], 0);
  271. return (this._size - margin * (items.length - 1)) / size;
  272. }
  273. private _getCost(
  274. items: IInfiniteGridItem[],
  275. i: number,
  276. j: number,
  277. ) {
  278. const style = this._style;
  279. const size1Name = style.size1;
  280. const size2Name = style.size2;
  281. const size = this._getSize(items.slice(i, j), size1Name, size2Name);
  282. const min = this.options.minSize || 0;
  283. const max = this.options.maxSize || Infinity;
  284. if (isFinite(max)) {
  285. // if this size is not in range, the cost increases sharply.
  286. if (size < min) {
  287. return Math.pow(size - min, 2) + Math.pow(max, 2);
  288. } else if (size > max) {
  289. return Math.pow(size - max, 2) + Math.pow(max, 2);
  290. } else {
  291. // if this size in range, the cost is negative or low.
  292. return Math.min(size - max, min - size);
  293. }
  294. }
  295. // if max is infinite type, caculate cost only with "min".
  296. if (size < min) {
  297. return Math.max(Math.pow(min, 2), Math.pow(size, 2));
  298. }
  299. return size - min;
  300. }
  301. private _setStyle(
  302. items: IInfiniteGridItem[],
  303. path: string[],
  304. outline: number[] = [],
  305. isAppend?: boolean,
  306. ) {
  307. const style = this._style;
  308. // if direction is vertical
  309. // startPos1 : top, endPos1 : bottom
  310. // size1 : height
  311. // startPos2 : left, endPos2 : right
  312. // size2 : width
  313. // if direction is horizontal
  314. // startPos1 : left, endPos1 : right
  315. // size1 : width
  316. // startPos2 : top, endPos2 : bottom
  317. // size2 : height
  318. const pos1Name = style.startPos1;
  319. const size1Name = style.size1;
  320. const pos2Name = style.startPos2;
  321. const size2Name = style.size2;
  322. const length = path.length;
  323. const margin = this.options.margin;
  324. const startPoint = outline[0] || 0;
  325. let endPoint = startPoint;
  326. let height = 0;
  327. for (let i = 0; i < length - 1; ++i) {
  328. const path1 = parseInt(path[i], 10);
  329. const path2 = parseInt(path[i + 1], 10);
  330. // pathItems(path1 to path2) are in 1 line.
  331. const pathItems = items.slice(path1, path2);
  332. const pathItemsLength = pathItems.length;
  333. const size1 = this._getSize(pathItems, size1Name, size2Name);
  334. const pos1 = endPoint;
  335. for (let j = 0; j < pathItemsLength; ++j) {
  336. const item = pathItems[j];
  337. const size2 = item.orgSize![size2Name] / item.orgSize![size1Name] * size1;
  338. // item has margin bottom and right.
  339. // first item has not margin.
  340. const prevItemRect = j === 0 ? 0 : pathItems[j - 1].rect;
  341. const pos2 = (prevItemRect ? prevItemRect[pos2Name] + prevItemRect[size2Name]! + margin : 0);
  342. item.rect = {
  343. [pos1Name]: pos1,
  344. [pos2Name]: pos2,
  345. [size1Name]: size1,
  346. [size2Name]: size2,
  347. } as any;
  348. }
  349. height += margin + size1;
  350. endPoint = startPoint + height;
  351. }
  352. const itemsLength = items.length;
  353. if (isAppend) {
  354. // previous group's end outline is current group's start outline
  355. return {
  356. start: [startPoint],
  357. end: [endPoint],
  358. };
  359. }
  360. // for prepend, only substract height from position.
  361. // always start is lower than end.
  362. for (let i = 0; i < itemsLength; ++i) {
  363. const item = items[i];
  364. // move items as long as height for prepend
  365. item.rect[pos1Name] -= height;
  366. }
  367. return {
  368. start: [startPoint - height],
  369. end: [startPoint], // endPoint - height = startPoint
  370. };
  371. }
  372. private _insert(items: IInfiniteGridItem[] = [], outline: number[] = [], isAppend?: boolean, cache?: boolean) {
  373. // this only needs the size of the item.
  374. const clone = cache ? items : cloneItems(items);
  375. return {
  376. items: clone,
  377. outlines: this._layout(clone, outline, isAppend),
  378. };
  379. }
  380. }
  381. export default JustifiedLayout;
comments powered by Disqus