/******************************************************************************* * Copyright (c) 2008, 2012 Stepan Rutz. * 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: * Stepan Rutz - initial implementation * Hallvard Trætteberg - further cleanup and development *******************************************************************************/ package org.eclipse.nebula.widgets.geomap.internal; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import org.eclipse.nebula.widgets.geomap.OsmTileServer; import org.eclipse.nebula.widgets.geomap.TileServer; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Display; /** * <p>License is EPL (Eclipse Public License) http://www.eclipse.org/legal/epl-v10.html. Contact at stepan.rutz@gmx.de</p> * * @author stepan.rutz, hal * @version $Revision$ */ public class GeoMapHelper implements GeoMapPositioned, GeoMapHelperListener { private final Display display; /** * Initializes a new <code>GeoMapHelper</code> with a specific display. * The display is used for async runnables and as device for images. * @param display the display */ private GeoMapHelper(Display display) { super(); this.display = display; } Display getDisplay() { return display; } /* basically not be changed, must be the same as GeoMapUtil's TILE_SIZE */ static final int TILE_SIZE = 256; private static final int DEFAULT_NUMBER_OF_IMAGEFETCHER_THREADS = 4; private Point mapSize = new Point(0, 0); private Point mapPosition = new Point(0, 0); private int zoom; AtomicLong zoomStamp = new AtomicLong(); private TileServer tileServer = OsmTileServer.TILESERVERS[0]; private int cacheSize; // must be readable from AsyncImage HashMap<TileRef, AsyncImage> cache; private BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(); private ThreadFactory threadFactory = new ThreadFactory() { public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("Async Image Loader " + thread.getId() + " " + System.identityHashCode(thread)); //$NON-NLS-1$ //$NON-NLS-2$ thread.setDaemon(true); return thread; } }; ThreadPoolExecutor executor = new ThreadPoolExecutor(DEFAULT_NUMBER_OF_IMAGEFETCHER_THREADS, 16, 2, TimeUnit.SECONDS, workQueue, threadFactory); private Color waitBackground, waitForeground; /** * Initializes a new <code>GeoMapHelper</code> for a specific display, position, zoom level and cache size. * @param display the <code>Display</code> to create images for. * @param mapPosition initial mapPosition. * @param zoom initial map zoom * @param cacheSize initial cache size, eg number of tile-images that are kept in cache * to prevent reloading from the network. */ @SuppressWarnings("serial") public GeoMapHelper(Display display, Point mapPosition, int zoom, int cacheSize) { this(display); this.cacheSize = cacheSize; this.cache = new LinkedHashMap<TileRef, AsyncImage>(cacheSize, 0.75f, true) { protected boolean removeEldestEntry(Map.Entry<TileRef, AsyncImage> eldest) { boolean remove = size() > GeoMapHelper.this.cacheSize; if (remove) { eldest.getValue().dispose(); } return remove; } }; waitBackground = new Color(display, 0x88, 0x88, 0x88); waitForeground = new Color(display, 0x77, 0x77, 0x77); setZoom(zoom); setMapPosition(mapPosition.x, mapPosition.y); } /** * Points the map to the provided graphics context (GC), which could be * the one provided in an SWT control paint request or one created for * rendering an SWT Image * @param gc the graphics context * @param clip the area that needs updating, could be null * @param size the size of the map area */ public void paint(GC gc, Rectangle clip, Point size) { long startTime = System.currentTimeMillis(); int tileCount = 0; int x0 = (int) Math.floor(((double) mapPosition.x) / TILE_SIZE); int y0 = (int) Math.floor(((double) mapPosition.y) / TILE_SIZE); int x1 = (int) Math.ceil(((double) mapPosition.x + size.x) / TILE_SIZE); int y1 = (int) Math.ceil(((double) mapPosition.y + size.y) / TILE_SIZE); int dy = y0 * TILE_SIZE - mapPosition.y; for (int y = y0; y < y1; y++) { int dx = x0 * TILE_SIZE - mapPosition.x; for (int x = x0; x < x1; x++) { if (clip == null || (dx + TILE_SIZE >= clip.x && dy + TILE_SIZE >= clip.y && dx <= clip.x + clip.width && dy <= clip.y + clip.height)) { paintTile(gc, dx, dy, x, y); } dx += TILE_SIZE; tileCount++; } dy += TILE_SIZE; } long endTime = System.currentTimeMillis(); for (InternalGeoMapListener listener : internalGeoMapListeners) { listener.mapPainted(tileCount, endTime - startTime); } for (InternalGeoMapListener listener : internalGeoMapListeners) { listener.tileCacheUpdated(cache.size(), this.cacheSize); } } void paintTile(GC gc, int dx, int dy, int x, int y) { boolean DRAW_IMAGES = true; boolean DEBUG = false; boolean DRAW_OUT_OF_BOUNDS = !false; boolean imageDrawn = false; int xTileCount = 1 << zoom; int yTileCount = 1 << zoom; boolean tileInBounds = x >= 0 && x < xTileCount && y >= 0 && y < yTileCount; boolean drawImage = DRAW_IMAGES && tileInBounds; if (drawImage) { TileRef tileRef = new TileRef(x, y, zoom); AsyncImage image = cache.get(tileRef); if (image == null) { image = new AsyncImage(this, tileRef, tileServer.getTileURL(tileRef)); cache.put(tileRef, image); } Image swtImage = image.getImage(getDisplay()); if (swtImage != null) { gc.drawImage(swtImage, dx, dy); imageDrawn = true; } else { // reuse tile from lower zoom level, i.e. half the resolution tileRef = new TileRef(x / 2, y / 2, zoom - 1); image = cache.get(tileRef); if (image != null) { swtImage = image.getImage(getDisplay()); if (swtImage != null) { gc.drawImage(swtImage, (x % 2 == 0 ? 0 : TILE_SIZE / 2), (y % 2 == 0 ? 0 : TILE_SIZE / 2), TILE_SIZE / 2, TILE_SIZE / 2, dx, dy, TILE_SIZE, TILE_SIZE); imageDrawn = true; } } } for (InternalGeoMapListener listener : internalGeoMapListeners) { listener.tilePainted(tileRef); } } if (DEBUG && (!imageDrawn && (tileInBounds || DRAW_OUT_OF_BOUNDS))) { gc.setBackground(display.getSystemColor(tileInBounds ? SWT.COLOR_GREEN : SWT.COLOR_RED)); gc.fillRectangle(dx + 4, dy + 4, TILE_SIZE - 8, TILE_SIZE - 8); gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK)); String s = "T " + x + ", " + y + (! tileInBounds ? " #" : ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ gc.drawString(s, dx + 4+ 8, dy + 4 + 12); } else if (!DEBUG && !imageDrawn && tileInBounds) { gc.setBackground(waitBackground); gc.fillRectangle(dx, dy, TILE_SIZE, TILE_SIZE); gc.setForeground(waitForeground); for (int yl = 0; yl < TILE_SIZE; yl += 32) { gc.drawLine(dx, dy + yl, dx + TILE_SIZE, dy + yl); } for (int xl = 0; xl < TILE_SIZE; xl += 32) { gc.drawLine(dx + xl, dy, dx + xl, dy + TILE_SIZE); } } } /** * Dispose internal data */ public void dispose() { waitBackground.dispose(); waitForeground.dispose(); } // /** * Gets the tile server used for fetching tiles * @return the tile server */ public TileServer getTileServer() { return tileServer; } /** * Sets the tile server used for fetching tiles * @param tileServer the new tile server to use */ public void setTileServer(TileServer tileServer) { this.tileServer = tileServer; cache.clear(); } // /* (non-Javadoc) * @see org.eclipse.nebula.widgets.geomap.internal.GeoMapPositioned#getMapPosition() */ public Point getMapPosition() { return new Point(mapPosition.x, mapPosition.y); } /* (non-Javadoc) * @see org.eclipse.nebula.widgets.geomap.internal.GeoMapPositioned#setMapPosition(int, int) */ public void setMapPosition(int x, int y) { mapPosition.x = x; mapPosition.y = y; } /* (non-Javadoc) * @see org.eclipse.nebula.widgets.geomap.internal.GeoMapPositioned#getMaxZoom() */ public int getMaxZoom() { return getTileServer().getMaxZoom(); } /* (non-Javadoc) * @see org.eclipse.nebula.widgets.geomap.internal.GeoMapPositioned#getZoom() */ public int getZoom() { return zoom; } /* (non-Javadoc) * @see org.eclipse.nebula.widgets.geomap.internal.GeoMapPositioned#setZoom(int) */ public void setZoom(int zoom) { zoomStamp.incrementAndGet(); this.zoom = Math.min(tileServer.getMaxZoom(), zoom); int size = TILE_SIZE * (1 << zoom); mapSize.x = size; mapSize.y = size; } // private List<GeoMapHelperListener> geoMapHelperListeners = new ArrayList<GeoMapHelperListener>(); public void tileUpdated(TileRef tileRef) { for (GeoMapHelperListener listener : geoMapHelperListeners) { listener.tileUpdated(tileRef); } } /** * Adds a GeoMapHelperListener that will be notified about tile updates * @param listener the GeoMapHelperListener */ public void addGeoMapHelperListener(GeoMapHelperListener listener) { geoMapHelperListeners.add(listener); } /** * Removes a GeoMapHelperListener * @param listener the GeoMapHelperListener */ public void removeGeoMapHelperListener(GeoMapHelperListener listener) { geoMapHelperListeners.remove(listener); } // private List<InternalGeoMapListener> internalGeoMapListeners = new ArrayList<InternalGeoMapListener>(); /** * Adds an InternalGeoMapListener that will be notified about painting and cache updates * @param listener the InternalGeoMapListener */ public void addInternalGeoMapListener(InternalGeoMapListener listener) { internalGeoMapListeners.add(listener); } /** * Removes an InternalGeoMapListener * @param listener the InternalGeoMapListener */ public void removeInternalGeoMapListener(InternalGeoMapListener listener) { internalGeoMapListeners.remove(listener); } }