]> gerrit.simantics Code Review - simantics/district.git/blobdiff - org.simantics.district.maps/src/org/simantics/maps/sg/MapNode.java
Share some projects for Simantics District
[simantics/district.git] / org.simantics.district.maps / src / org / simantics / maps / sg / MapNode.java
diff --git a/org.simantics.district.maps/src/org/simantics/maps/sg/MapNode.java b/org.simantics.district.maps/src/org/simantics/maps/sg/MapNode.java
new file mode 100644 (file)
index 0000000..01eab3a
--- /dev/null
@@ -0,0 +1,583 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Association for Decentralized Information Management
+ * in Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
+package org.simantics.maps.sg;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Composite;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.GraphicsConfiguration;
+import java.awt.Image;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.ImageObserver;
+import java.awt.image.VolatileImage;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.simantics.maps.WebService;
+import org.simantics.maps.osm.OSMTileProvider;
+import org.simantics.maps.pojo.TileJobQueue;
+import org.simantics.maps.tile.IFilter;
+import org.simantics.maps.tile.ITileJobQueue;
+import org.simantics.maps.tile.ITileListener;
+import org.simantics.maps.tile.ITileProvider;
+import org.simantics.maps.tile.TileCache;
+import org.simantics.maps.tile.TileKey;
+import org.simantics.scenegraph.g2d.G2DNode;
+import org.simantics.scenegraph.g2d.G2DRenderingHints;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public class MapNode extends G2DNode implements ITileListener  {
+
+    private static final long serialVersionUID = 2490944880914577411L;
+
+    private final double MAP_SCALE          = 1;
+    private final int    MAX_TILE_LEVEL     = 19;
+    private final int    TILE_PIXEL_SIZE    = 256;
+    private final int    VIEWBOX_QUIET_TIME = 500;
+
+    protected Boolean enabled = true;
+
+    enum TileState {
+        NOT_LOADED,
+        LOADING,
+        LOADED,
+        NOT_AVAILABLE
+    }
+
+    class TileLevel extends HashSet<TileKey> {
+        private static final long serialVersionUID = 5743763238677223952L;
+    }
+
+    static class TileTraverser {
+        List<TileKey> tile = new ArrayList<TileKey>(4);
+        List<Integer> quadrant = new ArrayList<Integer>(4);
+
+        static int getTileQuadrant(TileKey k) {
+            int x = k.getX();
+            int y = k.getY();
+            if ((x & 1) == 0) {
+                return ((y & 1) == 0) ? 1 : 2;
+            }
+            return ((y & 1) == 0) ? 0 : 3;
+        }
+
+        void clear() {
+            tile.clear();
+            quadrant.clear();
+        }
+
+        void add(TileKey tile) {
+            this.tile.add(tile);
+            this.quadrant.add(getTileQuadrant(tile));
+        }
+
+        int size() {
+            return tile.size();
+        }
+
+        TileKey getTile(int i) {
+            return tile.get(i);
+        }
+
+        int getQuadrant(int i) {
+            return quadrant.get(i);
+        }
+    }
+
+    public class ObjectHolder<T>
+    {
+        private T o = null;
+        public T get() {
+            return o;
+        }
+        public void set(T o) {
+            this.o = o;
+        }
+    }
+
+    public final class Pair<T1, T2> {
+        public final T1 first;
+        public final T2 second;
+        public Pair(T1 first, T2 second) {
+            this.first = first;
+            this.second = second;
+        }
+    }
+
+    private TileCache                      tileCache;
+
+    // For delaying the retrieval of map data until the viewport remains steady
+    private ScheduledFuture<?>             pendingTileTask;
+
+    private Map<Integer, TileLevel>        neededTiles          = new HashMap<Integer, TileLevel>();
+
+    private TileTraverser                  tileTraverser        = new TileTraverser();
+
+    private ITileJobQueue                  job                  = null;
+
+    private Map<TileKey, TileState>        tileStates           = new Hashtable<TileKey, TileState>();
+    private ObjectHolder<VolatileImage>    notLoadedImage       = new ObjectHolder<VolatileImage>();
+    private ObjectHolder<VolatileImage>    loadingImage         = new ObjectHolder<VolatileImage>();
+    private ObjectHolder<VolatileImage>    notAvailableImage    = new ObjectHolder<VolatileImage>();
+
+    private Font                           textFont             = new Font(Font.SANS_SERIF, Font.BOLD, 40);
+
+       private AffineTransform                oldTransform         = new AffineTransform();
+    private final ScheduledExecutorService scheduler            = Executors.newScheduledThreadPool(1);
+
+    private Composite                      normalComposite      = AlphaComposite.SrcOver.derive(1f);
+    private Composite                      parentDataComposite  = AlphaComposite.SrcOver.derive(0.75f);
+
+    private Rectangle2D                    helperRect           = new Rectangle2D.Double();
+
+    ImageObserver observer = new ImageObserver() {
+        @Override
+        public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
+//            System.out.format("imageUpdate(%s, %d, %d, %d, %d, %d)\n", img.toString(), infoflags, x, y, width, height);
+            return true;
+        }
+    };
+
+    private int scale;
+
+    public void init() {
+        try {
+            ITileProvider provider = new OSMTileProvider(new WebService("http://localhost:8080/osm-bright/"), TILE_PIXEL_SIZE);
+
+            // Try to load eclipse specific implementation of TileJobQueue, if it doesn't exist, fall back to pojo implementation 
+            try {
+                Class<?> proxyClass = (Class<?>) Class.forName("org.simantics.maps.eclipse.TileJobQueue");
+                job = (ITileJobQueue)proxyClass.newInstance();
+            } catch (ClassNotFoundException e1) {
+            } catch (InstantiationException e) {
+            } catch (IllegalAccessException e) {
+            }
+            // AppletProxyUtil should exist always..
+            if(job == null) {
+                job = new TileJobQueue();
+            }
+
+            // Cache is also eclipse specific...
+            ITileProvider cachedProvider = null;
+            try {
+                Class<?> proxyClass = (Class<?>) Class.forName("org.simantics.maps.eclipse.DiskCachingTileProvider");
+                cachedProvider = (ITileProvider)proxyClass.getConstructor(ITileProvider.class, Boolean.class).newInstance(provider, false);
+            } catch (ClassNotFoundException e1) {
+            } catch (InstantiationException e) {
+            } catch (IllegalAccessException e) {
+            }
+
+            if(cachedProvider != null) {
+                job.setTileProvider(cachedProvider);
+            } else {
+                job.setTileProvider(provider);
+            }
+
+            this.tileCache = new TileCache(job);
+            this.tileCache.addTileListener(this);
+        } catch (MalformedURLException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (URISyntaxException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch(Exception e ) {
+            e.printStackTrace();
+        }
+    }
+
+    @SyncField("enabled")
+    public void setEnabled(Boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    @Override
+    public void render(Graphics2D g2d) {
+        if (!enabled)
+            return;
+
+        Graphics2D g = (Graphics2D)g2d.create();
+
+        AffineTransform tr = (AffineTransform)g.getTransform().clone();
+
+        double scaleX = Math.abs(tr.getScaleX());
+        double scaleY = Math.abs(tr.getScaleY());
+        if (scaleX <= 0 || scaleY <= 0) {
+            // Make sure that we don't end up in an eternal loop below.
+            return;
+        }
+        double offsetX = -tr.getTranslateX();
+        double offsetY = -tr.getTranslateY();
+
+        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+        Rectangle2D sp = localToControl(new Rectangle2D.Double(0.0, 0.0, 1.0, 1.0));
+        Rectangle2D b = (Rectangle2D)((Rectangle)g.getRenderingHint(G2DRenderingHints.KEY_CONTROL_BOUNDS)).getBounds2D(); // getClipBounds is not accurate enough, use original clipbounds and scale here
+
+        Rectangle2D viewbox = new Rectangle2D.Double(offsetX/scaleX, offsetY/scaleY, b.getWidth()/sp.getWidth(), b.getHeight()/sp.getHeight()); //g.getClipBounds();
+        if(viewbox == null) return; // FIXME
+
+        double smallerViewboxDimension = viewbox.getWidth() < viewbox.getHeight() ? viewbox.getWidth() * MAP_SCALE : viewbox.getHeight() * MAP_SCALE;
+        int level = 0;
+        double tileSize = get360Scaled() * MAP_SCALE*2;
+        while (level < MAX_TILE_LEVEL) {
+            double ratio = smallerViewboxDimension / tileSize;
+            if (ratio >= 0.85) {
+                break;
+            }
+            tileSize *= 0.5;
+            level++;
+        }
+
+        /*
+         *  To convert y-coordinates to map coordinates in ruler, use:
+         *    double val = (y-offsetY)/scaleY;
+         *    val = Math.toDegrees(Math.atan(Math.sinh(Math.toRadians(val))));
+         *    String str = formatValue(val);
+         */
+
+        double minx = Math.min(get180Scaled(), Math.max(viewbox.getMinX(), -get180Scaled()));
+        double maxx = Math.min(get180Scaled(), Math.max(viewbox.getMaxX(), -get180Scaled()));
+
+        double miny = Math.min(get360Scaled(), Math.max(viewbox.getMinY()+get180Scaled(), 0));
+        double maxy = Math.min(get360Scaled(), Math.max(viewbox.getMaxY()+get180Scaled(), 0));
+
+        g.setTransform(new AffineTransform());
+        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+
+        int levels = (1 << level);
+
+        // http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
+        int left = (int)Math.floor( (minx + get180Scaled()) / get360Scaled() * (1<<level) );
+        int right = (int)Math.floor( (maxx + get180Scaled()) / get360Scaled() * (1<<level) );
+        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) );
+        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) );
+
+        double tsx = get360Scaled() / (double)levels; // Size of tile on zoom level
+        for(int tx = left; tx <= right; tx++) {
+            if(tx < 0 || tx >= levels) continue;
+            for(int ty = top; ty <= bottom; ty++) {
+                if(ty < 0 || ty >= levels) continue;
+                TileKey tile = new TileKey(level, tx, ty);
+                double y = (double)ty - (double)levels/2; // In level 0 we have only one tile
+                paintTile(tileCache, g, tr, tile, tx*tsx-get180Scaled(), y*tsx, tsx);
+            }
+        }
+        g.dispose();
+
+        // Check if transform has changed
+        transformChanged(tr, level);
+    }
+
+    private double get360Scaled() {
+        return 360 * scale;
+    }
+    
+    private double get180Scaled() {
+        return 180 * scale;
+    }
+
+    @Override
+    public Rectangle2D getBoundsInLocal() {
+        return null;
+    }
+
+    private void paintTile(TileCache cache, Graphics2D g, AffineTransform userToScreenTransform, TileKey tile, double tx, double ty, double tileSize) {
+        GraphicsConfiguration gc = g.getDeviceConfiguration();
+
+        Image img = null;
+        boolean usingParentData = false;
+        helperRect.setFrame(0, 0, TILE_PIXEL_SIZE, TILE_PIXEL_SIZE);
+
+        TileState state = tileStates.get(tile);
+        if (state == null)
+            state = TileState.NOT_LOADED;
+
+        switch (state) {
+            case NOT_LOADED:
+            case LOADED:
+            {
+                // The same logic applies both when the data has not been loaded
+                // or the data has been loaded previously, but may have been
+                // flushed from the cache.
+                img = cache.peek(tile);
+                if (img == null) {
+                    // Either the data has never existed before or the cache has
+                    // flushed this tile. Mark it as needed and try to use
+                    // parent image instead.
+                    needTile(tile);
+
+                    Pair<TileKey, Image> p = findFirstAvailableParentTile(cache, tile, tileTraverser);
+                    if (p != null) {
+                        img = p.second;
+                        helperRect.setFrame(0, 0, img.getWidth(observer), img.getHeight(observer));
+                        traverseRectangle(tileTraverser, helperRect);
+                        usingParentData = true;
+                    } else {
+                        img = getNotLoadedImage(gc);
+                    }
+                } else {
+                    helperRect.setFrame(0, 0, img.getWidth(observer), img.getHeight(observer));
+                }
+                break;
+            }
+            case LOADING: {
+                Pair<TileKey, Image> p = findFirstAvailableParentTile(cache, tile, tileTraverser);
+                if (p != null) {
+                    img = p.second;
+                    helperRect.setFrame(0, 0, img.getWidth(observer), img.getHeight(observer));
+                    traverseRectangle(tileTraverser, helperRect);
+                    usingParentData = true;
+                } else {
+                    img = getLoadingImage(gc);
+                }
+                break;
+            }
+            case NOT_AVAILABLE: {
+                img = getMissingImage(gc);
+                break;
+            }
+            default:
+                throw new Error("Invalid tile state: " + state);
+        }
+
+        // Calculate where these user space coordinates are in the screen
+        // space and given them to drawImage().
+        Point2D     helperPoint1  = new Point2D.Double(tx, ty);
+        Point2D     helperPoint2  = new Point2D.Double(tx + tileSize, ty + tileSize);
+        userToScreenTransform.transform(helperPoint1, helperPoint1);
+        userToScreenTransform.transform(helperPoint2, helperPoint2);
+
+        int dx1 = (int) Math.round(helperPoint1.getX());
+        int dy1 = (int) Math.round(helperPoint1.getY());
+        int dx2 = (int) Math.round(helperPoint2.getX());
+        int dy2 = (int) Math.round(helperPoint2.getY());
+
+        int sx1 = (int) Math.round(helperRect.getMinX());
+        int sy1 = (int) Math.round(helperRect.getMinY());
+        int sx2 = (int) Math.round(helperRect.getMaxX());
+        int sy2 = (int) Math.round(helperRect.getMaxY());
+
+        if (usingParentData) {
+            g.setComposite(parentDataComposite);
+        } else {
+            g.setComposite(normalComposite);
+        }
+        g.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer);
+    }
+
+    private void transformChanged(AffineTransform newTransform, final int level) {
+        // Cancel data fetching if the viewport changes.
+        AffineTransform ato = (AffineTransform) oldTransform;
+        AffineTransform atn = (AffineTransform) newTransform;
+
+        double s1 = -1;
+        if (ato != null)
+            s1 = ato.getScaleX();
+        double s2 = atn.getScaleX();
+        double ds = s2 - s1;
+        if (Math.abs(ds) > 1e-6) {
+            if (pendingTileTask != null) {
+                pendingTileTask.cancel(false);
+                pendingTileTask = null;
+            }
+            Runnable r = new Runnable() {
+                @Override
+                public void run() {
+                    // There was a quiet period of [delay] milliseconds.
+                    // Now its time to start fetching the map data that has
+                    // piled up into the queue.
+                    loadNeededTiles(level);
+                }
+            };
+            pendingTileTask = scheduler.schedule(r, VIEWBOX_QUIET_TIME, TimeUnit.MILLISECONDS);
+            oldTransform = (AffineTransform)newTransform.clone();
+        } else {
+            loadNeededTiles(level);
+        }
+    }
+
+    private void loadNeededTiles(final int level) {
+        TileCache cache = this.tileCache;
+
+        // Remove all tile queries from the job queue that do not match the
+        // requested tile level. This helps speed up loading of the right tiles
+        // that should be shown to the user ASAP instead of spending time
+        // loading invisible tiles.
+        cache.filterJobQueue(new IFilter<TileKey>() {
+            @Override
+            public boolean select(TileKey tile) {
+                boolean result = (tile.getLevel() == level);
+                //if (!result)
+                //    System.out.println("FILTERED TILE: " + tile);
+                return result;
+            }
+        });
+
+        // Schedule retrieval of each tile on the current level.
+        TileLevel l = null;
+        synchronized (neededTiles) {
+            l = neededTiles.get(level);
+            neededTiles.clear();
+        }
+        if (l != null && !l.isEmpty()) {
+            for (TileKey k : l) {
+               tileStates.put(k, TileState.LOADING);
+                tileCache.get(k);
+            }
+        }
+    }
+
+    void needTile(TileKey t) {
+        synchronized (neededTiles) {
+            int level = t.getLevel();
+            TileLevel l = neededTiles.get(level);
+            if (l == null) {
+                l = new TileLevel();
+                neededTiles.put(level, l);
+            }
+            l.add(t);
+        }
+    }
+
+    private Pair<TileKey, Image> findFirstAvailableParentTile(TileCache cache, TileKey tile, TileTraverser traverser) {
+        traverser.clear();
+        traverser.add(tile);
+
+        TileKey parent = getParentTile(tile);
+        while (parent != null) {
+            Image img = cache.peek(parent);
+            if (img != null)
+                return new Pair<TileKey, Image>(parent, img);
+            traverser.add(parent);
+            parent = getParentTile(parent);
+        }
+        return null;
+    }
+
+    private TileKey getParentTile(TileKey k) {
+        if (k.getLevel() == 0)
+            return null;
+        return new TileKey(k.getLevel() - 1, k.getX() / 2, k.getY() / 2);
+    }
+
+    private void traverseRectangle(TileTraverser traverser, Rectangle2D rect) {
+        for (int i = traverser.size() - 1; i >= 0; --i) {
+            traverseToQuadrant(rect, traverser.getQuadrant(i));
+        }
+    }
+
+    private Rectangle2D traverseToQuadrant(Rectangle2D r, int quadrant) {
+        boolean left = quadrant == 1 || quadrant == 2;
+        boolean top = quadrant == 1 || quadrant == 0;
+        r.setFrame(
+                left ? r.getMinX() : r.getCenterX(),
+                top ? r.getMinY() : r.getCenterY(),
+                r.getWidth() / 2.0,
+                r.getHeight() / 2.0);
+        return r;
+    }
+
+    private synchronized VolatileImage getNotLoadedImage(GraphicsConfiguration gc) {
+        return validateImageWithCenteredText(gc, notLoadedImage, TILE_PIXEL_SIZE, "NOT LOADED", Color.LIGHT_GRAY, Color.BLACK);
+    }
+
+    private synchronized VolatileImage getLoadingImage(GraphicsConfiguration gc) {
+        return validateImageWithCenteredText(gc, loadingImage, TILE_PIXEL_SIZE, "LOADING", Color.BLUE, Color.BLACK);
+    }
+
+    private synchronized VolatileImage getMissingImage(GraphicsConfiguration gc) {
+        return validateImageWithCenteredText(gc, notAvailableImage, TILE_PIXEL_SIZE, "NOT AVAILABLE", Color.RED, Color.DARK_GRAY);
+    }
+
+    private void drawImageWithCenteredText(VolatileImage image, String str, Font font, Color color, Color bgColor) {
+        Graphics2D target = image.createGraphics();
+        Rectangle2D bb = target.getFontMetrics(font).getStringBounds(str, target);
+
+        target.setBackground(new Color(255,255,255,0));
+        int w = image.getWidth();
+        int h = image.getHeight();
+
+        target.setColor(Color.BLACK);
+        target.fillRect(0, 0, w, h);
+        target.setColor(bgColor);
+        target.fillRect(2, 2, w - 3, h - 3);
+        target.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        target.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+        target.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
+        target.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+        target.setColor(color);
+        target.setFont(font);
+        target.drawString(str, (float) (((double)w - bb.getWidth()) / 2), (float) (((double)h + bb.getHeight()) / 2));
+        target.dispose();
+    }
+
+    private VolatileImage validateImageWithCenteredText(GraphicsConfiguration gc, ObjectHolder<VolatileImage> container, int size, String text, Color textColor, Color bgColor) {
+        VolatileImage image = container.get();
+        if (image != null) {
+            int validateResult = image.validate(gc);
+            if (validateResult == VolatileImage.IMAGE_INCOMPATIBLE) {
+                image = null;
+            } else if (validateResult == VolatileImage.IMAGE_OK) {
+                return image;
+            } else if (validateResult == VolatileImage.IMAGE_RESTORED) {
+                drawImageWithCenteredText(image, text, textFont, textColor, bgColor);
+            }
+        }
+        if (image == null) {
+            image = gc.createCompatibleVolatileImage(size, size);
+            container.set(image);
+            drawImageWithCenteredText(image, text, textFont, textColor, bgColor);
+        }
+        return image;
+    }
+
+    @Override
+    public void tileCanceled(TileKey key) {
+        tileStates.put(key, TileState.NOT_LOADED);
+    }
+
+    @Override
+    public void tileFailed(TileKey key, Throwable e) {
+        tileStates.put(key, TileState.NOT_AVAILABLE);
+        repaint();
+    }
+
+    @Override
+    public void tileUpdated(TileKey key, Image image) {
+        tileStates.put(key, TileState.LOADED);
+        repaint();
+    }
+
+    public void setScale(int scale) {
+        this.scale = scale;
+    }
+
+}
\ No newline at end of file