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