/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License 3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
******************************************************************************/
package com.opendoorlogistics.core.gis.map.background;
import gnu.trove.map.hash.TByteIntHashMap;
import gnu.trove.map.hash.TIntByteHashMap;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import org.apache.commons.io.FilenameUtils;
import org.mapsforge.core.graphics.Canvas;
import org.mapsforge.core.graphics.TileBitmap;
import org.mapsforge.map.awt.AwtGraphicFactory;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.queue.Job;
import org.mapsforge.map.layer.renderer.DatabaseRenderer;
import org.mapsforge.map.layer.renderer.RendererJob;
import org.mapsforge.map.model.DisplayModel;
import org.mapsforge.map.reader.MapDataStore;
import org.mapsforge.map.reader.MapFile;
import org.mapsforge.map.reader.MultiMapDataStore;
import org.mapsforge.map.reader.MultiMapDataStore.DataPolicy;
import org.mapsforge.map.rendertheme.ExternalRenderTheme;
import org.mapsforge.map.rendertheme.InternalRenderTheme;
import org.mapsforge.map.rendertheme.XmlRenderTheme;
import org.mapsforge.map.rendertheme.rule.RenderThemeFuture;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.Tile;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.TileFactory;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.TileFactoryInfo;
import com.opendoorlogistics.core.AppConstants;
import com.opendoorlogistics.core.cache.ApplicationCache;
import com.opendoorlogistics.core.cache.RecentlyUsedCache;
import com.opendoorlogistics.core.utils.images.CompressedImage;
import com.opendoorlogistics.core.utils.images.CompressedImage.CompressedType;
import com.opendoorlogistics.core.utils.io.RelativeFiles;
import com.opendoorlogistics.core.utils.strings.Strings;
class MapsforgeTileFactory extends TileFactory {
private static final int TILE_SIZE = 256;
private static final float TEXT_SCALE = 1.0f;
private final MapDataStore mapDatabase;
private final LinkedList<Tile> toCreate = new LinkedList<>();
private final DatabaseRenderer databaseRenderer;
private final XmlRenderTheme renderTheme;
private final DisplayModel model;
private final FadeConfig fadeColour;
private final ZoomLevelConverter zoomLevelConverter;
private ExecutorService service;
private static XmlRenderTheme getRenderTheme(String xmlRenderThemeFilename){
if(Strings.isEmpty(xmlRenderThemeFilename)==false){
File renderThemeFile = RelativeFiles.validateRelativeFiles(xmlRenderThemeFilename, AppConstants.ODL_CONFIG_DIR);
if (renderThemeFile != null) {
try {
return new ExternalRenderTheme(renderThemeFile.getAbsoluteFile());
} catch (Exception e) {
// just return the default theme
}
}
}
return InternalRenderTheme.OSMARENDER;
}
MapsforgeTileFactory(TileFactoryInfo info, String xmlRenderThemeFilename,MapDataStore mapDatabase, FadeConfig fadeColour) {
super(info);
this.fadeColour =fadeColour;
this.mapDatabase = mapDatabase;
zoomLevelConverter = new ZoomLevelConverter(info);
databaseRenderer = new DatabaseRenderer(mapDatabase, AwtGraphicFactory.INSTANCE,createDummyTileCacheForMapsforgeLabelPlacementAlgorithm());
renderTheme =getRenderTheme(xmlRenderThemeFilename);
model = new DisplayModel();
model.setFixedTileSize(TILE_SIZE);
model.setBackgroundColor(backgroundMapColour().getRGB());
// use single thread at the moment as DatabaseRenderer is probably single threaded
service = Executors.newFixedThreadPool(1, new ThreadFactory() {
private int count = 0;
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "mapsforge-tile-pool-" + count++);
t.setPriority(Thread.MIN_PRIORITY);
t.setDaemon(true);
return t;
}
});
}
private static Color backgroundMapColour() {
return Color.BLUE;
}
// private XmlRenderTheme changeBackgroundInRenderTheme(final XmlRenderTheme theme) {
//
// InputStream inputStream = null;
// try {
// inputStream = theme.getRenderThemeAsStream();
//
// DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
//
// dbf.setValidating(false);
// dbf.setIgnoringComments(false);
// dbf.setIgnoringElementContentWhitespace(true);
// dbf.setNamespaceAware(true);
//
// DocumentBuilder db = null;
// db = dbf.newDocumentBuilder();
// Document doc = db.parse(inputStream);
// if (doc.getChildNodes().getLength() > 0) {
// Node node = doc.getChildNodes().item(0);
//
// if (Strings.equalsStd(node.getNodeName(), "renderTheme")) {
// NamedNodeMap attributes = node.getAttributes();
// for (int i = 0; i < attributes.getLength(); i++) {
// Node attribNode = attributes.item(i);
// if (attribNode != null && Attr.class.isInstance(attribNode)) {
// Attr attr = (Attr) attribNode;
// if (Strings.equalsStd(attr.getNodeName(), "map-background")) {
// attr.setValue("#1010ff");
// }
// }
// }
//
// DOMSource source = new DOMSource(doc);
// StringWriter xmlAsWriter = new StringWriter();
//
// StreamResult result = new StreamResult(xmlAsWriter);
//
// TransformerFactory.newInstance().newTransformer().transform(source, result);
//
// // write changes
// final ByteArrayInputStream stream = new ByteArrayInputStream(xmlAsWriter.toString().getBytes("UTF-8"));
//
// return new XmlRenderTheme() {
//
// @Override
// public InputStream getRenderThemeAsStream() throws FileNotFoundException {
// return stream;
// }
//
// @Override
// public String getRelativePathPrefix() {
// return theme.getRelativePathPrefix();
// }
// };
// }
// }
//
// return theme;
//
// } catch (Exception e) {
// throw new RuntimeException(e);
// } finally {
// // if (renderThemeHandler.renderTheme != null) {
// // renderThemeHandler.renderTheme.destroy();
// // }
// IOUtils.closeQuietly(inputStream);
// }
//
// }
private String getTileId(int x, int y, int zoom) {
StringBuilder builder = new StringBuilder();
builder.append(x);
builder.append(",");
builder.append(y);
builder.append(",");
builder.append(zoom);
return builder.toString();
}
@Override
public synchronized Tile getTile(int x, int y, int zoom) {
// get from cache if exists
BufferedImage img = getCachedTileImage(x, y, zoom);
// create tile (which can have a null image initially)
Tile ret = createTileFromImg(x, y, zoom, img);
if (img == null) {
startLoading(ret);
}
return ret;
}
@Override
public BufferedImage renderSynchronously(int x, int y, int zoom) {
BufferedImage ret = getCachedTileImage(x, y, zoom);
if(ret==null){
Future<BufferedImage> future = submitAsCallable(new Tile(x, y, zoom));
try {
ret = future.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return ret;
}
/**
* @param x
* @param y
* @param zoom
* @param img
* @return
*/
private Tile createTileFromImg(int x, int y, int zoom, final BufferedImage img) {
Tile ret = new Tile(x, y, zoom) {
@Override
public BufferedImage getImage() {
return img;
}
@Override
public synchronized boolean isLoaded() {
return img != null;
}
@Override
public synchronized boolean isLoading() {
return img == null;
}
};
return ret;
}
@Override
public void dispose() {
if (service != null) {
service.shutdown();
service = null;
}
databaseRenderer.destroy();
mapDatabase.close();
}
@Override
protected synchronized void startLoading(Tile tile) {
// check not already pending
for (Tile pending : toCreate) {
if (isSameTile(tile, pending)) {
return;
}
}
// add to front
toCreate.addFirst(tile);
service.submit(new MultiTileCreator());
}
/**
* Submit the tile creation task as a callable so we can get its result in the calling thread
* @param tile
* @return
*/
private synchronized Future<BufferedImage> submitAsCallable(Tile tile){
return service.submit(new SingleTileCreator(tile));
}
/**
* @param a
* @param b
* @return
*/
private boolean isSameTile(Tile a, Tile b) {
return b.getX() == a.getX() && b.getY() == a.getY() && b.getZoom() == a.getZoom();
}
private class SingleTileCreator implements Callable<BufferedImage>{
final Tile tile;
SingleTileCreator(Tile tile) {
this.tile = tile;
}
@Override
public BufferedImage call() {
// get mapsforge zoom from jxmapviewer2 zoom (they use different conventions)
byte mapsforgeZoom = zoomLevelConverter.getMapsforge(tile.getZoom());
// load the render them
RenderThemeFuture rtf = new RenderThemeFuture(AwtGraphicFactory.INSTANCE, renderTheme, model);
rtf.run();
// render the mapsforge tile
org.mapsforge.core.model.Tile mtile = new org.mapsforge.core.model.Tile(tile.getX(), tile.getY(), mapsforgeZoom, TILE_SIZE);
RendererJob job = new RendererJob(mtile, mapDatabase, rtf, model, TEXT_SCALE, true, false);
TileBitmap bitmap = databaseRenderer.executeJob(job);
// copy it over onto an image (CompressedImage needs TYPE_INT_ARGB and anyway we can't access the buffered image internal to the tile)
BufferedImage image = new BufferedImage(TILE_SIZE, TILE_SIZE, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = (Graphics2D)image.getGraphics();
g.setClip(0, 0, TILE_SIZE, TILE_SIZE);
g.setColor(Color.WHITE);
g.fillRect(0, 0, TILE_SIZE, TILE_SIZE);
Canvas canvas = (Canvas) AwtGraphicFactory.createGraphicContext(g);
canvas.drawBitmap(bitmap, 0, 0);
if(fadeColour!=null){
BackgroundMapUtils.renderFade(g,fadeColour.getColour());
}
g.dispose();
if(fadeColour!=null){
image = BackgroundMapUtils.greyscale(image, fadeColour.getGreyscale());
}
// TEST save to file
// ImageUtils.toPNGFile(image, new File("C:\\temp\\MapsforgeOutput\\" + System.currentTimeMillis() + ".png"));
// add to cache
CompressedImage compressed = new CompressedImage(image, CompressedType.LZ4);
cacheImage(tile.getX(), tile.getY(), tile.getZoom(), compressed);
// remove from pending after adding from cache (so can't be added twice)
removeTile(tile);
return image;
}
}
private class MultiTileCreator implements Runnable {
@Override
public void run() {
// Keep on getting tiles until there's no more...
Tile tile = pollTopPending();
while (tile != null) {
BufferedImage image = new SingleTileCreator(tile).call();
// tell listeners
fireTileLoadedEvent(createTileFromImg(tile.getX(), tile.getY(), tile.getZoom(), image));
// get next
tile = pollTopPending();
}
}
}
private synchronized Tile pollTopPending() {
if (toCreate.size() > 0) {
return toCreate.poll();
}
return null;
}
private synchronized void removeTile(Tile tile) {
Iterator<Tile> it = toCreate.iterator();
while (it.hasNext()) {
Tile other = it.next();
if (isSameTile(tile, other)) {
it.remove();
}
}
}
private synchronized BufferedImage getCachedTileImage(int x, int y, int zoom) {
String id = getTileId(x, y, zoom);
RecentlyUsedCache cache = ApplicationCache.singleton().get(ApplicationCache.MAPSFORGE_BACKGROUND_TILES);
CompressedImage compressed = (CompressedImage) cache.get(id);
final BufferedImage img = compressed != null ? compressed.getBufferedImage() : null;
return img;
}
// private final HashMap<String, CompressedImage> cache = new HashMap<>();
private synchronized void cacheImage(int x, int y, int zoom, CompressedImage compressed) {
RecentlyUsedCache cache = ApplicationCache.singleton().get(ApplicationCache.MAPSFORGE_BACKGROUND_TILES);
cache.put(getTileId(x, y, zoom), compressed, compressed.getSizeBytes());
// cache.put(getTileId(x,y,zoom), compressed);
}
/**
* The mapsforge label placement algorithm needs to know what tiles are cached.
* We create a dummy tile cache object which links to our real cache, so we can test this.
* @return
*/
private TileCache createDummyTileCacheForMapsforgeLabelPlacementAlgorithm() {
TileCache dummyCache = new TileCache() {
@Override
public void setWorkingSet(Set<Job> workingSet) {
throw new UnsupportedOperationException();
}
@Override
public void put(Job key, TileBitmap bitmap) {
throw new UnsupportedOperationException();
}
@Override
public TileBitmap getImmediately(Job key) {
throw new UnsupportedOperationException();
}
@Override
public int getCapacityFirstLevel() {
throw new UnsupportedOperationException();
}
@Override
public int getCapacity() {
throw new UnsupportedOperationException();
}
@Override
public TileBitmap get(Job key) {
throw new UnsupportedOperationException();
}
@Override
public void destroy() {
throw new UnsupportedOperationException();
}
@Override
public boolean containsKey(Job key) {
int odlZoom = zoomLevelConverter.getODL(key.tile.zoomLevel);
return getCachedTileImage(key.tile.tileX, key.tile.tileY, odlZoom)!=null;
}
@Override
public void purge() {
// TODO Auto-generated method stub
}
};
return dummyCache;
}
// private byte getMapsforgeInternalZoomLevel(int ODLZoomLevel) {
// byte mapsforgeZoom;
// long nbTiles = getInfo().getMapWidthInTilesAtZoom(ODLZoomLevel);
// for (mapsforgeZoom = 0; mapsforgeZoom < 255; mapsforgeZoom++) {
// long maxMapsforgeTileNb = org.mapsforge.core.model.Tile.getMaxTileNumber(mapsforgeZoom);
// if (maxMapsforgeTileNb == nbTiles - 1) {
// break;
// }
// }
//
// if (mapsforgeZoom == 255) {
// throw new RuntimeException("Cannot match zoom levels between mapforge and jxmapviewer2.");
// }
// return mapsforgeZoom;
// }
// private int getODLZoomLevel(int mapsforgeInternalZoomLevel){
// TileFactoryInfo info = getInfo();
// for(int i =info.getMinimumZoomLevel() ; i < info.getMaximumZoomLevel() ; i++){
//
// }
// }
private static class ZoomLevelConverter{
TIntByteHashMap odlToMapsforge = new TIntByteHashMap();
TByteIntHashMap mapsforgeToODL = new TByteIntHashMap();
ZoomLevelConverter(TileFactoryInfo info){
for(int i =info.getMinimumZoomLevel() ; i <= info.getMaximumZoomLevel() ; i++){
byte mapsforgeZoom;
long nbTiles = info.getMapWidthInTilesAtZoom(i);
for (mapsforgeZoom = 0; mapsforgeZoom < 255; mapsforgeZoom++) {
long maxMapsforgeTileNb = org.mapsforge.core.model.Tile.getMaxTileNumber(mapsforgeZoom);
if (maxMapsforgeTileNb == nbTiles - 1) {
break;
}
}
if (mapsforgeZoom == 255) {
throw new RuntimeException("Cannot match zoom levels between mapforge and jxmapviewer2.");
}
odlToMapsforge.put(i, mapsforgeZoom);
mapsforgeToODL.put(mapsforgeZoom, i);
}
}
byte getMapsforge(int odl){
return odlToMapsforge.get(odl);
}
int getODL(byte mapsforge){
return mapsforgeToODL.get(mapsforge);
}
}
@Override
public boolean isRenderedOffline() {
return true;
}
static MapDataStore openMapsforgeDb(String filename) {
File file = null;
if (Strings.isEmpty(filename)) {
// to do.. assume loading all files in the mapsforge directory
file = new File(AppConstants.MAPSFORGE_DIRECTORY).getAbsoluteFile();
}else{
file = RelativeFiles.validateRelativeFiles(filename, AppConstants.MAPSFORGE_DIRECTORY);
}
if (file == null || !file.exists()) {
return null;
}
// if its a directory, load all the ones from the directory
MultiMapDataStore ret = new MultiMapDataStore(DataPolicy.RETURN_ALL);
if(file.isDirectory()){
for(File child : file.listFiles()){
String ext = FilenameUtils.getExtension(child.getAbsolutePath());
if(ext!=null && ext.toLowerCase().equals("map")){
try {
MapFile mf= new MapFile(child);
ret.addMapDataStore(mf, false, false);
} catch (Exception e) {
}
}
}
}
else{
try {
MapFile mf= new MapFile(file);
ret.addMapDataStore(mf, false, false);
} catch (Exception e) {
}
}
return ret;
}
}