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