]> gerrit.simantics Code Review - simantics/district.git/blob - org.simantics.district.maps/src/org/simantics/maps/sg/MapNode.java
Fixed most warnings from district codebase after JavaSE-11 switch
[simantics/district.git] / org.simantics.district.maps / src / org / simantics / maps / sg / MapNode.java
1 /*******************************************************************************
2  * Copyright (c) 2012 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.maps.sg;
13
14 import java.awt.AlphaComposite;
15 import java.awt.Color;
16 import java.awt.Composite;
17 import java.awt.Font;
18 import java.awt.Graphics2D;
19 import java.awt.GraphicsConfiguration;
20 import java.awt.Image;
21 import java.awt.Rectangle;
22 import java.awt.RenderingHints;
23 import java.awt.geom.AffineTransform;
24 import java.awt.geom.Point2D;
25 import java.awt.geom.Rectangle2D;
26 import java.awt.image.ImageObserver;
27 import java.awt.image.VolatileImage;
28 import java.net.MalformedURLException;
29 import java.net.URISyntaxException;
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.Hashtable;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.concurrent.Executors;
37 import java.util.concurrent.ScheduledExecutorService;
38 import java.util.concurrent.ScheduledFuture;
39 import java.util.concurrent.TimeUnit;
40
41 import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
42 import org.simantics.maps.MapScalingTransform;
43 import org.simantics.maps.WebService;
44 import org.simantics.maps.osm.OSMTileProvider;
45 import org.simantics.maps.pojo.TileJobQueue;
46 import org.simantics.maps.prefs.MapsClientPreferences;
47 import org.simantics.maps.tile.IFilter;
48 import org.simantics.maps.tile.ITileJobQueue;
49 import org.simantics.maps.tile.ITileListener;
50 import org.simantics.maps.tile.ITileProvider;
51 import org.simantics.maps.tile.TileCache;
52 import org.simantics.maps.tile.TileKey;
53 import org.simantics.scenegraph.g2d.G2DNode;
54 import org.simantics.scenegraph.g2d.G2DRenderingHints;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * @author Tuukka Lehtonen
60  */
61 public class MapNode extends G2DNode implements ITileListener  {
62
63     private static final long serialVersionUID = 2490944880914577411L;
64
65     private static final Logger LOGGER = LoggerFactory.getLogger(MapNode.class);
66
67     //private final double MAP_SCALE          = 1;
68     //private final int    MAX_TILE_LEVEL     = 19;
69     private final int    TILE_PIXEL_SIZE    = 256;
70     private final int    VIEWBOX_QUIET_TIME = 500;
71
72     protected Boolean enabled = true;
73     protected Color backgroundColor;
74
75     enum TileState {
76         NOT_LOADED,
77         LOADING,
78         LOADED,
79         NOT_AVAILABLE
80     }
81
82     class TileLevel extends HashSet<TileKey> {
83         private static final long serialVersionUID = 5743763238677223952L;
84     }
85
86     static class TileTraverser {
87         List<TileKey> tile = new ArrayList<TileKey>(4);
88         List<Integer> quadrant = new ArrayList<Integer>(4);
89
90         static int getTileQuadrant(TileKey k) {
91             int x = k.getX();
92             int y = k.getY();
93             if ((x & 1) == 0) {
94                 return ((y & 1) == 0) ? 1 : 2;
95             }
96             return ((y & 1) == 0) ? 0 : 3;
97         }
98
99         void clear() {
100             tile.clear();
101             quadrant.clear();
102         }
103
104         void add(TileKey tile) {
105             this.tile.add(tile);
106             this.quadrant.add(getTileQuadrant(tile));
107         }
108
109         int size() {
110             return tile.size();
111         }
112
113         TileKey getTile(int i) {
114             return tile.get(i);
115         }
116
117         int getQuadrant(int i) {
118             return quadrant.get(i);
119         }
120     }
121
122     public class ObjectHolder<T>
123     {
124         private T o = null;
125         public T get() {
126             return o;
127         }
128         public void set(T o) {
129             this.o = o;
130         }
131     }
132
133     public final class Pair<T1, T2> {
134         public final T1 first;
135         public final T2 second;
136         public Pair(T1 first, T2 second) {
137             this.first = first;
138             this.second = second;
139         }
140     }
141
142     private TileCache                      tileCache;
143
144     // For delaying the retrieval of map data until the viewport remains steady
145     private ScheduledFuture<?>             pendingTileTask;
146
147     private Map<Integer, TileLevel>        neededTiles          = new HashMap<Integer, TileLevel>();
148
149     private TileTraverser                  tileTraverser        = new TileTraverser();
150
151     private ITileJobQueue                  job                  = null;
152
153     private Map<TileKey, TileState>        tileStates           = new Hashtable<TileKey, TileState>();
154     private ObjectHolder<VolatileImage>    notLoadedImage       = new ObjectHolder<VolatileImage>();
155     private ObjectHolder<VolatileImage>    loadingImage         = new ObjectHolder<VolatileImage>();
156     private ObjectHolder<VolatileImage>    notAvailableImage    = new ObjectHolder<VolatileImage>();
157
158     private Font                           textFont             = new Font(Font.SANS_SERIF, Font.BOLD, 40);
159
160         private AffineTransform                oldTransform         = new AffineTransform();
161     private final ScheduledExecutorService scheduler            = Executors.newScheduledThreadPool(1);
162
163     private Composite                      normalComposite      = AlphaComposite.SrcOver.derive(1f);
164     private Composite                      parentDataComposite  = AlphaComposite.SrcOver.derive(0.75f);
165
166     private Rectangle2D                    helperRect           = new Rectangle2D.Double();
167
168     ImageObserver observer = new ImageObserver() {
169         @Override
170         public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
171 //            System.out.format("imageUpdate(%s, %d, %d, %d, %d, %d)\n", img.toString(), infoflags, x, y, width, height);
172             return true;
173         }
174     };
175
176     private IPreferenceChangeListener listener;
177
178     public void init() {
179         try {
180             OSMTileProvider provider = new OSMTileProvider(new WebService(computeUrl()), TILE_PIXEL_SIZE);
181
182             listener = event -> {
183                 // if tiles or style change we want to flush the tile cache
184                 if (MapsClientPreferences.P_CURRENT_MBTILES.equals(event.getKey()) || MapsClientPreferences.P_CURRENT_TM2STYLE.equals(event.getKey())) {
185                     if (tileCache != null) {
186                         tileCache.clear();
187                     }
188                     try {
189                         provider.setWebService(new WebService(computeUrl()));
190                     } catch (MalformedURLException | URISyntaxException e) {
191                         LOGGER.error("Unable to update WebService with new url", e);
192                     }
193                 }
194             };
195             MapsClientPreferences.addPreferenceChangeListenerMapsServer(listener);
196
197             // Try to load eclipse specific implementation of TileJobQueue, if it doesn't exist, fall back to pojo implementation 
198             try {
199                 Class<?> proxyClass = (Class<?>) Class.forName("org.simantics.maps.eclipse.TileJobQueue");
200                 job = (ITileJobQueue)proxyClass.getDeclaredConstructor().newInstance();
201             } catch (ClassNotFoundException e1) {
202             } catch (InstantiationException e) {
203             } catch (IllegalAccessException e) {
204             }
205             // AppletProxyUtil should exist always..
206             if(job == null) {
207                 job = new TileJobQueue();
208             }
209
210             // Cache is also eclipse specific...
211             ITileProvider cachedProvider = null;
212             try {
213                 Class<?> proxyClass = (Class<?>) Class.forName("org.simantics.maps.eclipse.DiskCachingTileProvider");
214                 cachedProvider = (ITileProvider)proxyClass.getConstructor(ITileProvider.class, Boolean.class).newInstance(provider, false);
215             } catch (ClassNotFoundException e1) {
216             } catch (InstantiationException e) {
217             } catch (IllegalAccessException e) {
218             }
219
220             if(cachedProvider != null) {
221                 job.setTileProvider(cachedProvider);
222             } else {
223                 job.setTileProvider(provider);
224             }
225
226             this.tileCache = new TileCache(job);
227             this.tileCache.addTileListener(this);
228         } catch(Exception e ) {
229             LOGGER.error("Failed to initialize MapNode", e);
230         }
231     }
232
233     private static String computeUrl() {
234      // Construct WebService from client properties
235         String url;
236         if (MapsClientPreferences.useBuiltinServer())
237             url = MapsClientPreferences.possibleBuiltinServerURL();
238         else 
239             url = MapsClientPreferences.tileserverURL();
240         if (!url.endsWith("/"))
241             url = url + "/";
242         return url;
243     }
244
245     @Override
246     public void cleanup() {
247         MapsClientPreferences.removePreferenceChangeListenerMapsServer(listener);
248         tileCache.clear();
249         job.clear();
250     }
251
252     @SyncField("enabled")
253     public void setEnabled(Boolean enabled) {
254         this.enabled = enabled;
255     }
256
257     @SyncField("backgroundColor")
258     public void setBackgroundColor(Color color) {
259         this.backgroundColor = color;
260     }
261
262     @Override
263     public void render(Graphics2D g2d) {
264
265         
266         AffineTransform ot = g2d.getTransform();
267         //System.err.println("ot " + ot);
268         g2d.transform(transform);
269         
270         AffineTransform tr = g2d.getTransform();
271         //System.err.println("tr " + tr);
272 //        Graphics2D g = (Graphics2D)g2d.create();
273 //        AffineTransform tr = (AffineTransform)g.getTransform().clone();
274         
275         double scaleX = Math.abs(tr.getScaleX());
276         //System.err.println("scaleX : " + scaleX);
277         double scaleY = Math.abs(tr.getScaleY());
278         if (scaleX <= 0 || scaleY <= 0) {
279             // Make sure that we don't end up in an eternal loop below.
280             return;
281         }
282         double offsetX = -tr.getTranslateX();
283         double offsetY = -tr.getTranslateY();
284
285         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
286
287         Rectangle2D sp = localToControl(new Rectangle2D.Double(0.0, 0.0, 1.0, 1.0));
288         Rectangle2D b = (Rectangle2D)((Rectangle)g2d.getRenderingHint(G2DRenderingHints.KEY_CONTROL_BOUNDS)).getBounds2D(); // getClipBounds is not accurate enough, use original clipbounds and scale here
289
290         Rectangle2D viewbox = new Rectangle2D.Double(offsetX/scaleX, offsetY/scaleY, b.getWidth()/sp.getWidth(), b.getHeight()/sp.getHeight()); //g.getClipBounds();
291         if (enabled) {
292             
293             int level = MapScalingTransform.zoomLevel(ot);
294             
295 //            double smallerViewboxDimension = viewbox.getWidth() < viewbox.getHeight() ? viewbox.getWidth() * MAP_SCALE : viewbox.getHeight() * MAP_SCALE;
296 //            int level = 0;
297 //            double tileSize = 360 * MAP_SCALE*2.5;
298 //            while (level < MAX_TILE_LEVEL) {
299 //                double ratio = smallerViewboxDimension / tileSize;
300 //                if (ratio >= 0.85) {
301 //                    break;
302 //                }
303 //                tileSize *= 0.5;
304 //                level++;
305 //            }
306             /*
307              *  To convert y-coordinates to map coordinates in ruler, use:
308              *    double val = (y-offsetY)/scaleY;
309              *    val = Math.toDegrees(Math.atan(Math.sinh(Math.toRadians(val))));
310              *    String str = formatValue(val);
311              */
312     
313             double minx = Math.min(180, Math.max(viewbox.getMinX(), -180));
314             double maxx = Math.min(180, Math.max(viewbox.getMaxX(), -180));
315     
316             double miny = Math.min(360, Math.max(viewbox.getMinY()+180, 0));
317             double maxy = Math.min(360, Math.max(viewbox.getMaxY()+180, 0));
318             //System.err.println("minx " + minx + " maxx " + maxx + " miny " + miny + " maxy " + maxy);
319             g2d.setTransform(new AffineTransform());
320             g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
321     
322             int levels = (1 << level);
323             //System.err.println("level " + level);
324             //System.err.println("levels " + levels);
325             // http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
326             int left = (int)Math.floor( (minx + 180) / 360 * (1<<level) );
327             int right = (int)Math.floor( (maxx + 180) / 360 * (1<<level) );
328             int top = (int)Math.floor(miny / 360 * (1<<level));//  (int)Math.floor( (1 - Math.log(Math.tan(Math.toRadians(miny)) + 1 / Math.cos(Math.toRadians(miny))) / Math.PI) / 2 * (1<<level) );
329             int bottom = (int)Math.floor(maxy / 360 * (1<<level));//  (int)Math.floor( (1 - Math.log(Math.tan(Math.toRadians(maxy)) + 1 / Math.cos(Math.toRadians(maxy))) / Math.PI) / 2 * (1<<level) );
330             //System.err.println("left " + left + " right " + right + " bottom " + bottom + " top " + top);
331             double tsx = 360 / (double)levels; // Size of tile on zoom level
332             //System.err.println("tileSize " + tsx);
333             for (int tx = left; tx <= right; tx++) {
334                 if (tx < 0 || tx >= levels)
335                     continue;
336                 for (int ty = top; ty <= bottom; ty++) {
337                     if (ty < 0 || ty >= levels)
338                         continue;
339                     TileKey tile = new TileKey(level, tx, ty);
340                     double y = (double) ty - (double) levels / 2; // In level 0 we have only one tile
341                     
342                     paintTile(tileCache, g2d, tr, tile, tx * tsx - 180, y * tsx, tsx);
343                 }
344             }
345     //        g.dispose();
346     
347             // Check if transform has changed
348             transformChanged(tr, level);
349         } else {
350             if (backgroundColor != null) {
351                 g2d.setTransform(new AffineTransform());
352                 Rectangle2D controlBounds = (Rectangle2D) g2d.getRenderingHint(G2DRenderingHints.KEY_CONTROL_BOUNDS);
353                 Color color = g2d.getColor();
354                 g2d.setColor(backgroundColor);
355                 g2d.fill(controlBounds);
356                 g2d.setColor(color);
357             }
358         }
359         g2d.setTransform(ot);
360     }
361
362     @Override
363     public Rectangle2D getBoundsInLocal() {
364         return null;
365     }
366
367     private void paintTile(TileCache cache, Graphics2D g, AffineTransform userToScreenTransform, TileKey tile, double tx, double ty, double tileSize) {
368         GraphicsConfiguration gc = g.getDeviceConfiguration();
369
370         Image img = null;
371         boolean usingParentData = false;
372         helperRect.setFrame(0, 0, TILE_PIXEL_SIZE, TILE_PIXEL_SIZE);
373
374         TileState state = tileStates.get(tile);
375         if (state == null)
376             state = TileState.NOT_LOADED;
377
378         switch (state) {
379             case NOT_LOADED:
380             case LOADED:
381             {
382                 // The same logic applies both when the data has not been loaded
383                 // or the data has been loaded previously, but may have been
384                 // flushed from the cache.
385                 img = cache.peek(tile);
386                 if (img == null) {
387                     // Either the data has never existed before or the cache has
388                     // flushed this tile. Mark it as needed and try to use
389                     // parent image instead.
390                     needTile(tile);
391
392                     Pair<TileKey, Image> p = findFirstAvailableParentTile(cache, tile, tileTraverser);
393                     if (p != null) {
394                         img = p.second;
395                         helperRect.setFrame(0, 0, img.getWidth(observer), img.getHeight(observer));
396                         traverseRectangle(tileTraverser, helperRect);
397                         usingParentData = true;
398                     } else {
399                         img = getNotLoadedImage(gc);
400                     }
401                 } else {
402                     helperRect.setFrame(0, 0, img.getWidth(observer), img.getHeight(observer));
403                 }
404                 break;
405             }
406             case LOADING: {
407                 Pair<TileKey, Image> p = findFirstAvailableParentTile(cache, tile, tileTraverser);
408                 if (p != null) {
409                     img = p.second;
410                     helperRect.setFrame(0, 0, img.getWidth(observer), img.getHeight(observer));
411                     traverseRectangle(tileTraverser, helperRect);
412                     usingParentData = true;
413                 } else {
414                     img = getLoadingImage(gc);
415                 }
416                 break;
417             }
418             case NOT_AVAILABLE: {
419                 img = getMissingImage(gc);
420                 break;
421             }
422             default:
423                 throw new Error("Invalid tile state: " + state);
424         }
425
426         // Calculate where these user space coordinates are in the screen
427         // space and given them to drawImage().
428         Point2D     helperPoint1  = new Point2D.Double(tx, ty);
429         Point2D     helperPoint2  = new Point2D.Double(tx + tileSize, ty + tileSize);
430         userToScreenTransform.transform(helperPoint1, helperPoint1);
431         userToScreenTransform.transform(helperPoint2, helperPoint2);
432
433         int dx1 = (int) Math.round(helperPoint1.getX());
434         int dy1 = (int) Math.round(helperPoint1.getY());
435         int dx2 = (int) Math.round(helperPoint2.getX());
436         int dy2 = (int) Math.round(helperPoint2.getY());
437
438         int sx1 = (int) Math.round(helperRect.getMinX());
439         int sy1 = (int) Math.round(helperRect.getMinY());
440         int sx2 = (int) Math.round(helperRect.getMaxX());
441         int sy2 = (int) Math.round(helperRect.getMaxY());
442
443         if (usingParentData) {
444             g.setComposite(parentDataComposite);
445         } else {
446             g.setComposite(normalComposite);
447         }
448         g.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer);
449     }
450
451     private void transformChanged(AffineTransform newTransform, final int level) {
452         // Cancel data fetching if the viewport changes.
453         AffineTransform ato = (AffineTransform) oldTransform;
454         AffineTransform atn = (AffineTransform) newTransform;
455
456         double s1 = -1;
457         if (ato != null)
458             s1 = ato.getScaleX();
459         double s2 = atn.getScaleX();
460         double ds = s2 - s1;
461         if (Math.abs(ds) > 1e-6) {
462             if (pendingTileTask != null) {
463                 pendingTileTask.cancel(false);
464                 pendingTileTask = null;
465             }
466             Runnable r = new Runnable() {
467                 @Override
468                 public void run() {
469                     // There was a quiet period of [delay] milliseconds.
470                     // Now its time to start fetching the map data that has
471                     // piled up into the queue.
472                     loadNeededTiles(level);
473                 }
474             };
475             pendingTileTask = scheduler.schedule(r, VIEWBOX_QUIET_TIME, TimeUnit.MILLISECONDS);
476             oldTransform = (AffineTransform)newTransform.clone();
477         } else {
478             loadNeededTiles(level);
479         }
480     }
481
482     private void loadNeededTiles(final int level) {
483         TileCache cache = this.tileCache;
484
485         // Remove all tile queries from the job queue that do not match the
486         // requested tile level. This helps speed up loading of the right tiles
487         // that should be shown to the user ASAP instead of spending time
488         // loading invisible tiles.
489         cache.filterJobQueue(new IFilter<TileKey>() {
490             @Override
491             public boolean select(TileKey tile) {
492                 boolean result = (tile.getLevel() == level);
493                 //if (!result)
494                 //    System.out.println("FILTERED TILE: " + tile);
495                 return result;
496             }
497         });
498
499         // Schedule retrieval of each tile on the current level.
500         TileLevel l = null;
501         synchronized (neededTiles) {
502             l = neededTiles.get(level);
503             neededTiles.clear();
504         }
505         if (l != null && !l.isEmpty()) {
506             for (TileKey k : l) {
507                 tileStates.put(k, TileState.LOADING);
508                 tileCache.get(k);
509             }
510         }
511     }
512
513     void needTile(TileKey t) {
514         synchronized (neededTiles) {
515             int level = t.getLevel();
516             TileLevel l = neededTiles.get(level);
517             if (l == null) {
518                 l = new TileLevel();
519                 neededTiles.put(level, l);
520             }
521             l.add(t);
522         }
523     }
524
525     private Pair<TileKey, Image> findFirstAvailableParentTile(TileCache cache, TileKey tile, TileTraverser traverser) {
526         traverser.clear();
527         traverser.add(tile);
528
529         TileKey parent = getParentTile(tile);
530         while (parent != null) {
531             Image img = cache.peek(parent);
532             if (img != null)
533                 return new Pair<TileKey, Image>(parent, img);
534             traverser.add(parent);
535             parent = getParentTile(parent);
536         }
537         return null;
538     }
539
540     private TileKey getParentTile(TileKey k) {
541         if (k.getLevel() == 0)
542             return null;
543         return new TileKey(k.getLevel() - 1, k.getX() / 2, k.getY() / 2);
544     }
545
546     private void traverseRectangle(TileTraverser traverser, Rectangle2D rect) {
547         for (int i = traverser.size() - 1; i >= 0; --i) {
548             traverseToQuadrant(rect, traverser.getQuadrant(i));
549         }
550     }
551
552     private Rectangle2D traverseToQuadrant(Rectangle2D r, int quadrant) {
553         boolean left = quadrant == 1 || quadrant == 2;
554         boolean top = quadrant == 1 || quadrant == 0;
555         r.setFrame(
556                 left ? r.getMinX() : r.getCenterX(),
557                 top ? r.getMinY() : r.getCenterY(),
558                 r.getWidth() / 2.0,
559                 r.getHeight() / 2.0);
560         return r;
561     }
562
563     private synchronized VolatileImage getNotLoadedImage(GraphicsConfiguration gc) {
564         return validateImageWithCenteredText(gc, notLoadedImage, TILE_PIXEL_SIZE, "NOT LOADED", Color.LIGHT_GRAY, Color.BLACK);
565     }
566
567     private synchronized VolatileImage getLoadingImage(GraphicsConfiguration gc) {
568         return validateImageWithCenteredText(gc, loadingImage, TILE_PIXEL_SIZE, "LOADING", Color.BLUE, Color.BLACK);
569     }
570
571     private synchronized VolatileImage getMissingImage(GraphicsConfiguration gc) {
572         return validateImageWithCenteredText(gc, notAvailableImage, TILE_PIXEL_SIZE, "NOT AVAILABLE", Color.RED, Color.DARK_GRAY);
573     }
574
575     private void drawImageWithCenteredText(VolatileImage image, String str, Font font, Color color, Color bgColor) {
576         Graphics2D target = image.createGraphics();
577         Rectangle2D bb = target.getFontMetrics(font).getStringBounds(str, target);
578
579         target.setBackground(new Color(255,255,255,0));
580         int w = image.getWidth();
581         int h = image.getHeight();
582
583         target.setColor(Color.BLACK);
584         target.fillRect(0, 0, w, h);
585         target.setColor(bgColor);
586         target.fillRect(2, 2, w - 3, h - 3);
587         target.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
588         target.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
589         target.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
590         target.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
591         target.setColor(color);
592         target.setFont(font);
593         target.drawString(str, (float) (((double)w - bb.getWidth()) / 2), (float) (((double)h + bb.getHeight()) / 2));
594         target.dispose();
595     }
596
597     private VolatileImage validateImageWithCenteredText(GraphicsConfiguration gc, ObjectHolder<VolatileImage> container, int size, String text, Color textColor, Color bgColor) {
598         VolatileImage image = container.get();
599         if (image != null) {
600             int validateResult = image.validate(gc);
601             if (validateResult == VolatileImage.IMAGE_INCOMPATIBLE) {
602                 image = null;
603             } else if (validateResult == VolatileImage.IMAGE_OK) {
604                 return image;
605             } else if (validateResult == VolatileImage.IMAGE_RESTORED) {
606                 drawImageWithCenteredText(image, text, textFont, textColor, bgColor);
607             }
608         }
609         if (image == null) {
610             image = gc.createCompatibleVolatileImage(size, size);
611             container.set(image);
612             drawImageWithCenteredText(image, text, textFont, textColor, bgColor);
613         }
614         return image;
615     }
616
617     @Override
618     public void tileCanceled(TileKey key) {
619         tileStates.put(key, TileState.NOT_LOADED);
620     }
621
622     @Override
623     public void tileFailed(TileKey key, Throwable e) {
624         tileStates.put(key, TileState.NOT_AVAILABLE);
625         repaint();
626     }
627
628     @Override
629     public void tileUpdated(TileKey key, Image image) {
630         tileStates.put(key, TileState.LOADED);
631         repaint();
632     }
633
634 }