--- /dev/null
+/*******************************************************************************
+ * 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<Provider> providers = new ArrayList<Provider>();
+
+ 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<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);
+ }
+ }
+
+ 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));
+ }
+ }
+
+}