1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
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
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
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
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
169 this.cachedThumbnails = Range.between(0, 0);
170 this.idToUrl.clear();
171 this.idToIndex.clear();
172 this.indexToThumbnail.clear();
173
174
175 getWidget().setThumbnailAmount(getState().elementAmount);
176
177
178
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
252
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
262
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
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 }