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.LazyLayout;
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.vaadin.client.communication.StateChangeEvent;
52 import com.vaadin.client.ui.AbstractComponentConnector;
53
54
55
56
57
58
59 public abstract class LazyLayoutConnector extends AbstractComponentConnector {
60
61 protected static final int THUMBNAIL_QUERY_RPC_DELAY = 200;
62
63 private static final Logger log = Logger.getLogger(LazyLayoutConnector.class.getSimpleName());
64
65 protected Range cachedThumbnails = Range.between(0, 0);
66
67 protected Map<Object, Integer> idToIndex = new HashMap<>();
68
69 protected Map<Integer, ThumbnailData> indexToThumbnail = new HashMap<>();
70
71 protected Map<String, String> idToUrl = new HashMap<>();
72
73 private boolean widgetInitialized = false;
74
75 protected boolean waitingData = false;
76
77 @Override
78 protected void init() {
79 super.init();
80 registerRpc(LazyLayoutClientRpc.class, new LazyLayoutClientRpc() {
81 @Override
82 public void addElements(List<ThumbnailData> data, int startingFrom) {
83 final Range received = Range.withLength(startingFrom, data.size());
84 final Range maxCacheRange = getMaxCacheRange();
85 final Range[] partition = received.partitionWith(maxCacheRange);
86 final Range newUsefulData = partition[1];
87 if (!newUsefulData.isEmpty()) {
88
89 for (int i = newUsefulData.getStart(); i < newUsefulData.getEnd(); i++) {
90 final ThumbnailData thumbnailData = data.get(i - startingFrom);
91 if (thumbnailData.isRealResource()) {
92 idToUrl.put(thumbnailData.getThumbnailId(), getResourceUrl(thumbnailData.getThumbnailId()));
93 }
94
95 indexToThumbnail.put(i, thumbnailData);
96 idToIndex.put(thumbnailData.getThumbnailId(), i);
97 }
98
99 final Range toPushToWidget = newUsefulData.restrictTo(getWidget().getDisplayedRange());
100 for (int i = toPushToWidget.getStart(); i < toPushToWidget.getEnd(); ++i) {
101 updateThumbnailContentAtIndex(i);
102 }
103
104
105 if (cachedThumbnails.isEmpty()) {
106 cachedThumbnails = newUsefulData;
107 } else {
108 purgeCache();
109 if (!cachedThumbnails.isEmpty()) {
110 cachedThumbnails = cachedThumbnails.combineWith(newUsefulData);
111 } else {
112 cachedThumbnails = newUsefulData;
113 }
114 }
115 }
116
117 waitingData = false;
118
119
120 serveThumbnails();
121 }
122
123 @Override
124 public void refresh() {
125 refreshViewport();
126 }
127 });
128
129 getLayoutManager().addElementResizeListener(getWidget().getElement(), e -> getWidget().resize());
130 }
131
132 @Override
133 public void onStateChanged(StateChangeEvent stateChangeEvent) {
134 super.onStateChanged(stateChangeEvent);
135
136 if (widgetInitialized && stateChangeEvent.hasPropertyChanged("size")) {
137 getWidget().initialize(getState().elementAmount, getState().offset, getState().size, getState().scaleRatio, getState().isFirstUpdate);
138 }
139
140 if (widgetInitialized && stateChangeEvent.hasPropertyChanged("selection")) {
141 updateSelection();
142 }
143
144 if (!widgetInitialized) {
145 Scheduler.get().scheduleDeferred(() -> {
146 widgetInitialized = true;
147 getWidget().initialize(getState().elementAmount, getState().offset, getState().size, getState().scaleRatio, getState().isFirstUpdate);
148 updateSelection();
149 });
150 }
151 }
152
153 protected void refreshViewport() {
154
155 this.cachedThumbnails = Range.between(0, 0);
156 this.idToUrl.clear();
157 this.idToIndex.clear();
158 this.indexToThumbnail.clear();
159
160
161 getWidget().setThumbnailAmount(getState().elementAmount);
162
163
164
165 cancelTimer();
166 queryThumbnails(getWidget().getDisplayedRange());
167 }
168
169 abstract void cancelTimer();
170
171 public EscalatorPanel getWidget() {
172 return (EscalatorPanel) super.getWidget();
173 }
174
175 @Override
176 public ThumbnailLayoutState getState() {
177 return (ThumbnailLayoutState) super.getState();
178 }
179
180 public void serveThumbnails() {
181 this.getLazyLayoutServerRpc().updateOffset(getWidget().getCurrentThumbnailOffset());
182
183 if (waitingData) {
184 return;
185 }
186
187 final Range newMinimumCachedRange = getMinCacheRange();
188 if (!newMinimumCachedRange.intersects(cachedThumbnails) || cachedThumbnails.isEmpty()) {
189 indexToThumbnail.clear();
190 idToIndex.clear();
191 idToUrl.clear();
192
193 cachedThumbnails = Range.between(0, 0);
194
195 queryThumbnails(getMaxCacheRange());
196 log.log(Level.FINEST, "Querying: " + getMaxCacheRange());
197 } else {
198 final Range intersection = newMinimumCachedRange.restrictTo(cachedThumbnails).restrictTo(getWidget().getDisplayedRange());
199 for (int i = intersection.getStart(); i < intersection.getEnd(); ++i) {
200 updateThumbnailContentAtIndex(i);
201 }
202
203 purgeCache();
204
205 if (!newMinimumCachedRange.isSubsetOf(cachedThumbnails)) {
206 final Range[] missingCachePartition = getMaxCacheRange().partitionWith(cachedThumbnails);
207 queryThumbnails(missingCachePartition[0]);
208 queryThumbnails(missingCachePartition[2]);
209 log.log(Level.FINEST, "Querying: " + missingCachePartition[0] + " and " + missingCachePartition[2]);
210 }
211 }
212 }
213
214 protected void purgeCache() {
215 final Range[] cachePartition = cachedThumbnails.partitionWith(getMaxCacheRange());
216 dropFromCache(cachePartition[0]);
217 cachedThumbnails = cachePartition[1];
218 dropFromCache(cachePartition[2]);
219 }
220
221 private void dropFromCache(Range range) {
222 for (int i = range.getStart(); i < range.getEnd(); i++) {
223 final ThumbnailData removed = indexToThumbnail.remove(i);
224
225 String thumbnailId = removed.getThumbnailId();
226 idToIndex.remove(thumbnailId);
227 idToUrl.remove(thumbnailId);
228 }
229 }
230
231 private void queryThumbnails(Range range) {
232 if (!range.isEmpty()) {
233 this.getLazyLayoutServerRpc().loadElements(range.getStart(), range.length(), cachedThumbnails.getStart(), cachedThumbnails.getEnd());
234 waitingData = true;
235 }
236 }
237
238
239
240
241
242 private Range getMinCacheRange() {
243 final Range displayedRange = getWidget().getDisplayedRange();
244 int cachePageSize = displayedRange.length();
245 return displayedRange.expand(cachePageSize / 2, cachePageSize / 2).restrictTo(getAvailableRange());
246 }
247
248
249
250
251
252 protected Range getMaxCacheRange() {
253 final Range displayedRange = getWidget().getDisplayedRange();
254 int cachePageSize = displayedRange.length();
255 return displayedRange.expand(cachePageSize, cachePageSize).restrictTo(getAvailableRange());
256 }
257
258
259
260
261 private Range getAvailableRange() {
262 return Range.between(0, getState().elementAmount);
263 }
264
265 @Override
266 abstract protected EscalatorPanel createWidget();
267
268 public void updateSelection() {
269 final ThumbnailLayoutState.SelectionModel selection = getState().selection;
270
271 final Range displayedRange = getWidget().getDisplayedRange();
272 final Range selectionBoundaries = selection.getSelectionBoundaries();
273 final List<Integer> indices = new LinkedList<>();
274
275 if (selectionBoundaries.intersects(displayedRange)) {
276 for (int selectedIndex : selection.selectedIndices) {
277 if (displayedRange.contains(selectedIndex)) {
278 indices.add(selectedIndex);
279 }
280 }
281 }
282 getWidget().setSelectedThumbnailsViaIndices(indices);
283 }
284
285 abstract void updateThumbnailContentAtIndex(int thumbnailAbsoluteIndex);
286
287 abstract LazyLayoutServerRpc getLazyLayoutServerRpc();
288 }