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