/******************************************************************************* * 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.tile; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Image; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.net.URI; import java.util.ArrayList; import java.util.List; import org.simantics.maps.ProvisionException; /** * @author Tuukka Lehtonen */ public class CompoundTileProvider implements ITileProvider { URI source; int tileSize; static class Provider { final ITileProvider provider; final int minTileLevel; final int maxTileLevel; public Provider(ITileProvider provider, int minTileLevel, int maxTileLevel) { this.provider = provider; this.minTileLevel = minTileLevel; this.maxTileLevel = maxTileLevel; } } List providers = new ArrayList(); public CompoundTileProvider(URI source, int tileSize) { this.source = source; this.tileSize = tileSize; } @Override public URI getSource() { return source; } @Override public Rectangle2D getExtent() { if (providers.isEmpty()) return new Rectangle2D.Double(); boolean first = true; Rectangle2D result = new Rectangle2D.Double(); for (Provider p : providers) { if (first) { result.setFrame(p.provider.getExtent()); first = false; } else { Rectangle2D.union(result, p.provider.getExtent(), result); } } return result; } public void addProvider(ITileProvider provider, int minTileLevel, int maxTileLevel) { if (minTileLevel > maxTileLevel) throw new IllegalArgumentException("minTileLevel (" + minTileLevel + ") cannot be larger than maxTileLevel" + maxTileLevel + ")"); providers.add(new Provider(provider, minTileLevel, maxTileLevel)); } @Override public Image get(TileKey key) throws ProvisionException { // Prefer the tile of a provider that has larger minZoom value when the requested tile is completely covered by the provider. // If the tile is only partially covered by the service with the larger minZoom, combine it with another provider's tile. int level = key.getLevel(); int x = key.getX(); int y = key.getY(); double xTiles = Math.pow(2, level + 1); double yTiles = Math.pow(2, level); if (level < 0) throw new IllegalArgumentException("invalid tile level " + level + " (tile=" + key +")"); if (x < 0 || x >= (int) xTiles) throw new IllegalArgumentException("tile x out of bounds " + x + " (tile=" + key +")"); if (y < 0 || y >= (int) yTiles) throw new IllegalArgumentException("tile y out of bounds " + y + " (tile=" + key +")"); Rectangle2D r = new Rectangle2D.Double(); double minx = -180; double miny = -90; double w = 360; double h = 180; double xdelta = w / xTiles; double ydelta = h / yTiles; double xx = x; double yy = yTiles - y - 1; r.setFrame( minx + xdelta*xx, miny + ydelta*yy, xdelta, ydelta ); BufferedImage result = null; Graphics2D g = null; try { for (Provider p : providers) { if (level < p.minTileLevel) { // System.out.println("Provider " + p.provider + " tile level too small: " + level + " < " + p.minTileLevel); continue; } Rectangle2D extent = p.provider.getExtent(); if (!r.intersects(extent)) { // System.out.println("Provider " + p.provider + ": " + r + " out of bounds: " + extent); continue; } if (result == null) { result = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_3BYTE_BGR); g = result.createGraphics(); } if (level > p.maxTileLevel) { // System.out.println("Provider " + p.provider + " tile level too large: " + level + " > " + p.maxTileLevel); Rectangle2D helperRect = new Rectangle2D.Double(0, 0, tileSize, tileSize); TileTraverser tileTraverser = new TileTraverser(); TileKey maxTile = findFirstAvailableParentTile(p.maxTileLevel, key, tileTraverser); Image img = p.provider.get(maxTile); helperRect.setFrame(0, 0, img.getWidth(null), img.getHeight(null)); traverseRectangle(tileTraverser, helperRect); 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()); g.drawImage(img, 0, 0, tileSize, tileSize, sx1, sy1, sx2, sy2, null); } else { Image src = p.provider.get(key); int srcW = src.getWidth(null); int srcH = src.getHeight(null); g.drawImage(src, 0, 0, tileSize, tileSize, 0, 0, srcW, srcH, null); } } } finally { if (g != null) g.dispose(); } return result != null ? result : getOutOfBoundsImage(); } BufferedImage outOfBoundsImage; private Image getOutOfBoundsImage() { if (outOfBoundsImage != null) return outOfBoundsImage; final int outOfBoundsImageSize = 1; BufferedImage image = new BufferedImage(outOfBoundsImageSize, outOfBoundsImageSize, BufferedImage.TYPE_3BYTE_BGR); Graphics2D g = image.createGraphics(); try { g.setColor(Color.CYAN); g.fillRect(0, 0, outOfBoundsImageSize, outOfBoundsImageSize); outOfBoundsImage = image; return image; } finally { g.dispose(); } } /** * Quadrants: * * 1|0 * --- * 2|3 * * @param k * @return */ TileKey getChildTile(TileKey k, int quadrant) { int ox = quadrant == 0 || quadrant == 3 ? 1 : 0; int oy = quadrant == 0 || quadrant == 1 ? 1 : 0; return new TileKey(k.getLevel() + 1, k.getX() * 2 + ox, k.getY() * 2 + oy); } TileKey getParentTile(TileKey k) { if (k.getLevel() == 0) return null; return new TileKey(k.getLevel() - 1, k.getX() / 2, k.getY() / 2); } 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; } int findTilePos(int level, double t, double tileSize) { // Change 0 to change the world origin. double dt = t - 0; double p = dt / tileSize; int tt = (int) Math.floor(p); return tt; } static class TileTraverser { List tile = new ArrayList(4); List quadrant = new ArrayList(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); } } private TileKey findFirstAvailableParentTile(int maxLevel, TileKey tile, TileTraverser traverser) { traverser.clear(); traverser.add(tile); TileKey parent = getParentTile(tile); while (parent != null) { if (parent.getLevel() <= maxLevel) return parent; traverser.add(parent); parent = getParentTile(parent); } return null; } private void traverseRectangle(TileTraverser traverser, Rectangle2D rect) { for (int i = traverser.size() - 1; i >= 0; --i) { traverseToQuadrant(rect, traverser.getQuadrant(i)); } } }