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