View Javadoc
1   /**
2    * This file Copyright (c) 2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.connector;
35  
36  import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.rpc.LazyLayoutClientRpc;
37  import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.rpc.LazyLayoutServerRpc;
38  import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.shared.ThumbnailData;
39  import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.widget.EscalatorPanel;
40  import info.magnolia.ui.vaadin.gwt.shared.Range;
41  import info.magnolia.ui.vaadin.layout.LazyThumbnailLayout;
42  
43  import java.util.HashMap;
44  import java.util.LinkedList;
45  import java.util.List;
46  import java.util.Map;
47  import java.util.logging.Level;
48  import java.util.logging.Logger;
49  
50  import com.google.gwt.core.client.Scheduler;
51  import com.google.gwt.user.client.Timer;
52  import com.vaadin.client.communication.RpcProxy;
53  import com.vaadin.client.communication.StateChangeEvent;
54  import com.vaadin.client.ui.AbstractComponentConnector;
55  import com.vaadin.shared.ui.Connect;
56  
57  /**
58   * Connector for {@link LazyThumbnailLayout}.
59   *
60   */
61  @Connect(LazyThumbnailLayout.class)
62  public class LazyLayoutConnector extends AbstractComponentConnector implements EscalatorPanel.Listener {
63      private final LazyLayoutServerRpc rpc = RpcProxy.create(LazyLayoutServerRpc.class, this);
64  
65      private Timer timer = new Timer() {
66          @Override
67          public void run() {
68              serveThumbnails();
69          }
70      };
71  
72      protected static final int THUMBNAIL_QUERY_RPC_DELAY = 200;
73  
74      private static final Logger log = Logger.getLogger(LazyLayoutConnector.class.getSimpleName());
75  
76      protected Range cachedThumbnails = Range.between(0, 0);
77  
78      protected Map<Object, Integer> idToIndex = new HashMap<>();
79  
80      protected Map<Integer, ThumbnailData> indexToThumbnail = new HashMap<>();
81  
82      protected Map<String, String> idToUrl = new HashMap<>();
83  
84      private boolean widgetInitialized = false;
85  
86      protected boolean waitingData = false;
87  
88      @Override
89      protected void init() {
90          super.init();
91          registerRpc(LazyLayoutClientRpc.class, new LazyLayoutClientRpc() {
92              @Override
93              public void addElements(List<ThumbnailData> data, int startingFrom) {
94                  final Range received = Range.withLength(startingFrom, data.size());
95                  final Range maxCacheRange = getMaxCacheRange();
96                  final Range[] partition = received.partitionWith(maxCacheRange);
97                  final Range newUsefulData = partition[1];
98                  if (!newUsefulData.isEmpty()) {
99                      // Update the parts that are actually inside
100                     for (int i = newUsefulData.getStart(); i < newUsefulData.getEnd(); i++) {
101                         final ThumbnailData thumbnailData = data.get(i - startingFrom);
102                         if (thumbnailData.isRealResource()) {
103                             idToUrl.put(thumbnailData.getThumbnailId(), getResourceUrl(thumbnailData.getThumbnailId()));
104                         }
105 
106                         indexToThumbnail.put(i, thumbnailData);
107                         idToIndex.put(thumbnailData.getThumbnailId(), i);
108                     }
109 
110                     final Range toPushToWidget = newUsefulData.restrictTo(getWidget().getDisplayedRange());
111                     for (int i = toPushToWidget.getStart(); i < toPushToWidget.getEnd(); ++i) {
112                         updateThumbnailContentAtIndex(i);
113                     }
114 
115                     // Potentially extend the range
116                     if (cachedThumbnails.isEmpty()) {
117                         cachedThumbnails = newUsefulData;
118                     } else {
119                         purgeCache();
120                         if (!cachedThumbnails.isEmpty()) {
121                             cachedThumbnails = cachedThumbnails.combineWith(newUsefulData);
122                         } else {
123                             cachedThumbnails = newUsefulData;
124                         }
125                     }
126                 }
127 
128                 waitingData = false;
129 
130                 // Eventually check whether all needed rows are now available
131                 serveThumbnails();
132             }
133 
134             @Override
135             public void refresh() {
136                 refreshViewport();
137             }
138         });
139 
140         getLayoutManager().addElementResizeListener(getWidget().getElement(), e -> getWidget().resize());
141     }
142 
143     @Override
144     public void onStateChanged(StateChangeEvent stateChangeEvent) {
145         super.onStateChanged(stateChangeEvent);
146 
147         EscalatorPanel widget = getWidget();
148         ThumbnailLayoutState state = getState();
149 
150         if (widgetInitialized && stateChangeEvent.hasPropertyChanged("size")) {
151             widget.initialize(state.elementAmount, state.offset, state.size, state.scaleRatio, state.isFirstUpdate);
152         }
153 
154         if (widgetInitialized && stateChangeEvent.hasPropertyChanged("selection")) {
155             updateSelection();
156         }
157 
158         if (!widgetInitialized) {
159             Scheduler.get().scheduleDeferred(() -> {
160                 widgetInitialized = true;
161                 widget.initialize(state.elementAmount, state.offset, state.size, state.scaleRatio, state.isFirstUpdate);
162                 updateSelection();
163             });
164         }
165     }
166 
167     protected void refreshViewport() {
168         // Clear caches and maps
169         this.cachedThumbnails = Range.between(0, 0);
170         this.idToUrl.clear();
171         this.idToIndex.clear();
172         this.indexToThumbnail.clear();
173 
174         // Reset thumbnail amount
175         getWidget().setThumbnailAmount(getState().elementAmount);
176 
177         // Previous call would trigger lazy update of the viewport, but we don't need it because we anyway
178         // are going to query the whole visible range => cancel the timer
179         cancelTimer();
180         queryThumbnails(getWidget().getDisplayedRange());
181     }
182 
183     public EscalatorPanel getWidget() {
184         return (EscalatorPanel) super.getWidget();
185     }
186 
187     @Override
188     public ThumbnailLayoutState getState() {
189         return (ThumbnailLayoutState) super.getState();
190     }
191 
192     public void serveThumbnails() {
193         this.getLazyLayoutServerRpc().updateOffset(getWidget().getCurrentThumbnailOffset());
194 
195         if (waitingData) {
196             return;
197         }
198 
199         final Range newMinimumCachedRange = getMinCacheRange();
200         if (!newMinimumCachedRange.intersects(cachedThumbnails) || cachedThumbnails.isEmpty()) {
201             indexToThumbnail.clear();
202             idToIndex.clear();
203             idToUrl.clear();
204 
205             cachedThumbnails = Range.between(0, 0);
206 
207             queryThumbnails(getMaxCacheRange());
208             log.log(Level.FINEST, "Querying: " + getMaxCacheRange());
209         } else {
210             final Range intersection = newMinimumCachedRange.restrictTo(cachedThumbnails).restrictTo(getWidget().getDisplayedRange());
211             for (int i = intersection.getStart(); i < intersection.getEnd(); ++i) {
212                 updateThumbnailContentAtIndex(i);
213             }
214 
215             purgeCache();
216 
217             if (!newMinimumCachedRange.isSubsetOf(cachedThumbnails)) {
218                 final Range[] missingCachePartition = getMaxCacheRange().partitionWith(cachedThumbnails);
219                 queryThumbnails(missingCachePartition[0]);
220                 queryThumbnails(missingCachePartition[2]);
221                 log.log(Level.FINEST, "Querying: " + missingCachePartition[0] + " and " + missingCachePartition[2]);
222             }
223         }
224     }
225 
226     protected void purgeCache() {
227         final Range[] cachePartition = cachedThumbnails.partitionWith(getMaxCacheRange());
228         dropFromCache(cachePartition[0]);
229         cachedThumbnails = cachePartition[1];
230         dropFromCache(cachePartition[2]);
231     }
232 
233     private void dropFromCache(Range range) {
234         for (int i = range.getStart(); i < range.getEnd(); i++) {
235             final ThumbnailData removed = indexToThumbnail.remove(i);
236 
237             String thumbnailId = removed.getThumbnailId();
238             idToIndex.remove(thumbnailId);
239             idToUrl.remove(thumbnailId);
240         }
241     }
242 
243     private void queryThumbnails(Range range) {
244         if (!range.isEmpty()) {
245             this.getLazyLayoutServerRpc().loadElements(range.getStart(), range.length(), cachedThumbnails.getStart(), cachedThumbnails.getEnd());
246             waitingData = true;
247         }
248     }
249 
250     /**
251      * @return how many thumbnails should be cached from both sides before we will have to ask server to
252      * load more (half of the page).
253      */
254     private Range getMinCacheRange() {
255         final Range displayedRange = getWidget().getDisplayedRange();
256         int cachePageSize = displayedRange.length();
257         return displayedRange.expand(cachePageSize / 2, cachePageSize / 2).restrictTo(getAvailableRange());
258     }
259 
260     /**
261      * @return how many thumbnails can be cached on either of sides before we start evicting them from the
262      * cache (full page by). Also this is the amount of thumbnails to be queried from teh server.
263      */
264     protected Range getMaxCacheRange() {
265         final Range displayedRange = getWidget().getDisplayedRange();
266         int cachePageSize = displayedRange.length();
267         return displayedRange.expand(cachePageSize, cachePageSize).restrictTo(getAvailableRange());
268     }
269 
270     /**
271      * @return
272      */
273     private Range getAvailableRange() {
274         return Range.between(0, getState().elementAmount);
275     }
276 
277     @Override
278     protected EscalatorPanel createWidget() {
279         final EscalatorPanelin/gwt/client/layout/lazylayout/widget/EscalatorPanel.html#EscalatorPanel">EscalatorPanel layout = new EscalatorPanel(this);
280         layout.setThumbnailService(new ThumbnailService() {
281             @Override
282             public void onViewportChanged(final Range requestedRange) {
283                 timer.schedule(THUMBNAIL_QUERY_RPC_DELAY);
284                 updateSelection();
285             }
286 
287             @Override
288             public void onThumbnailsScaled(float ratio) {
289                 rpc.setScaleRatio(ratio);
290             }
291         });
292         return layout;
293     }
294 
295     public void updateSelection() {
296         final ThumbnailLayoutState.SelectionModel selection = getState().selection;
297 
298         final Range displayedRange = getWidget().getDisplayedRange();
299         final Range selectionBoundaries = selection.getSelectionBoundaries();
300         final List<Integer> indices = new LinkedList<>();
301 
302         if (selectionBoundaries.intersects(displayedRange)) {
303             for (int selectedIndex : selection.selectedIndices) {
304                 if (displayedRange.contains(selectedIndex)) {
305                     indices.add(selectedIndex);
306                 }
307             }
308         }
309         getWidget().setSelectedThumbnailsViaIndices(indices);
310     }
311 
312     public void updateThumbnailContentAtIndex(int thumbnailAbsoluteIndex) {
313         final ThumbnailData thumbnailData = indexToThumbnail.get(thumbnailAbsoluteIndex);
314         EscalatorPanel widget = getWidget();
315 
316         widget.updateImageSource(idToUrl.get(thumbnailData.getThumbnailId()), thumbnailAbsoluteIndex);
317         widget.updateImageCaption(thumbnailData.getCaption(), thumbnailAbsoluteIndex);
318     }
319 
320     @Override
321     public void onThumbnailClicked(int index, boolean isMetaKeyPressed, boolean isShiftKeyPressed) {
322         rpc.onElementsSelected(index, isMetaKeyPressed, isShiftKeyPressed);
323     }
324 
325     @Override
326     public void onThumbnailRightClicked(int index, int xPos, int yPos) {
327         rpc.onElementRightClicked(index, xPos, yPos);
328     }
329 
330     @Override
331     public void onThumbnailDoubleClicked(int index) {
332         rpc.onElementDoubleClicked(index);
333     }
334 
335     public LazyLayoutServerRpc getLazyLayoutServerRpc() {
336         return rpc;
337     }
338 
339     void cancelTimer() {
340         timer.cancel();
341     }
342 }