CustomTileFactory.java 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. package de.tu_darmstadt.informatik.tk.scopviz.ui.mapView;
  2. import java.awt.image.BufferedImage;
  3. import java.io.ByteArrayInputStream;
  4. import java.io.ByteArrayOutputStream;
  5. import java.io.IOException;
  6. import java.io.InputStream;
  7. import java.lang.ref.SoftReference;
  8. import java.net.URI;
  9. import java.net.URISyntaxException;
  10. import java.net.URL;
  11. import java.net.URLConnection;
  12. import java.util.Comparator;
  13. import java.util.HashMap;
  14. import java.util.Map;
  15. import java.util.concurrent.BlockingQueue;
  16. import java.util.concurrent.ExecutorService;
  17. import java.util.concurrent.Executors;
  18. import java.util.concurrent.PriorityBlockingQueue;
  19. import java.util.concurrent.ThreadFactory;
  20. import javax.imageio.ImageIO;
  21. import javax.swing.SwingUtilities;
  22. import org.jxmapviewer.util.ProjectProperties;
  23. import org.jxmapviewer.viewer.AbstractTileFactory;
  24. import org.jxmapviewer.viewer.Tile;
  25. import org.jxmapviewer.viewer.TileCache;
  26. import org.jxmapviewer.viewer.TileFactoryInfo;
  27. import org.jxmapviewer.viewer.util.GeoUtil;
  28. /**
  29. * Custom tile factory for handling connection problems
  30. *
  31. * @author Dominik Renkel
  32. *
  33. */
  34. public class CustomTileFactory extends AbstractTileFactory {
  35. /**
  36. * Note that the name and version are actually set by Gradle so there is no
  37. * need to bump a version manually when new release is made.
  38. */
  39. private static final String DEFAULT_USER_AGENT = ProjectProperties.INSTANCE.getName() + "/"
  40. + ProjectProperties.INSTANCE.getVersion();
  41. /**
  42. * userAgent needed to request data from OpenStreetMap servers
  43. */
  44. private String userAgent = DEFAULT_USER_AGENT;
  45. /**
  46. * standard thread count, that are used to load tiles
  47. */
  48. private int threadPoolSize = 4;
  49. /**
  50. * Handling Threads
  51. */
  52. private ExecutorService service;
  53. /**
  54. * saves loaded tiles
  55. */
  56. private Map<String, Tile> tileMap = new HashMap<String, Tile>();
  57. /**
  58. * Actual Loaded Tile chache
  59. */
  60. private TileCache cache = new TileCache();
  61. /**
  62. * constructor with super reference
  63. *
  64. * @param info
  65. */
  66. public CustomTileFactory(TileFactoryInfo info) {
  67. super(info);
  68. }
  69. /**
  70. * Returns the tile that is located at the given tilePoint for this zoom.
  71. * For example, if getMapSize() returns 10x20 for this zoom, and the
  72. * tilePoint is (3,5), then the appropriate tile will be located and
  73. * returned.
  74. */
  75. @Override
  76. public CustomTile getTile(int x, int y, int zoom) {
  77. return getTile(x, y, zoom, true);
  78. }
  79. /**
  80. * Get a tile like above, but load image when not already in TileCache
  81. *
  82. * @param tpx
  83. * @param tpy
  84. * @param zoom
  85. * @param eagerLoad
  86. * load all tiles with same priority
  87. * @return
  88. */
  89. private CustomTile getTile(int tpx, int tpy, int zoom, boolean eagerLoad) {
  90. // wrap the tiles horizontally --> mod the X with the max width
  91. // and use that
  92. int tileX = tpx;// tilePoint.getX();
  93. int numTilesWide = (int) getMapSize(zoom).getWidth();
  94. if (tileX < 0) {
  95. tileX = numTilesWide - (Math.abs(tileX) % numTilesWide);
  96. }
  97. tileX = tileX % numTilesWide;
  98. int tileY = tpy;
  99. String url = getInfo().getTileUrl(tileX, tileY, zoom);
  100. Tile.Priority pri = Tile.Priority.High;
  101. if (!eagerLoad) {
  102. pri = Tile.Priority.Low;
  103. }
  104. CustomTile tile;
  105. if (!tileMap.containsKey(url)) {
  106. if (!GeoUtil.isValidTile(tileX, tileY, zoom, getInfo())) {
  107. tile = new CustomTile(tileX, tileY, zoom);
  108. } else {
  109. tile = new CustomTile(tileX, tileY, zoom, url, pri, this);
  110. startLoading(tile);
  111. }
  112. tileMap.put(url, tile);
  113. } else {
  114. tile = (CustomTile) tileMap.get(url);
  115. // if its in the map but is low and isn't loaded yet
  116. // but we are in high mode
  117. if (tile.getPriority() == Tile.Priority.Low && eagerLoad && !tile.isLoaded()) {
  118. // tile.promote();
  119. promote(tile);
  120. }
  121. }
  122. return tile;
  123. }
  124. /** ==== threaded tile loading stuff === */
  125. /**
  126. * Thread pool for loading the tiles
  127. */
  128. private BlockingQueue<Tile> tileQueue = new PriorityBlockingQueue<Tile>(5, new Comparator<Tile>() {
  129. @Override
  130. public int compare(Tile o1, Tile o2) {
  131. if (o1.getPriority() == Tile.Priority.Low && o2.getPriority() == Tile.Priority.High) {
  132. return 1;
  133. }
  134. if (o1.getPriority() == Tile.Priority.High && o2.getPriority() == Tile.Priority.Low) {
  135. return -1;
  136. }
  137. return 0;
  138. }
  139. });
  140. /**
  141. * @return the tile cache
  142. */
  143. @Override
  144. public TileCache getTileCache() {
  145. return cache;
  146. }
  147. /**
  148. * @param cache
  149. * the tile cache
  150. */
  151. @Override
  152. public void setTileCache(TileCache cache) {
  153. this.cache = cache;
  154. }
  155. /**
  156. * Subclasses may override this method to provide their own executor
  157. * services. This method will be called each time a tile needs to be loaded.
  158. * Implementations should cache the ExecutorService when possible.
  159. *
  160. * @return ExecutorService to load tiles with
  161. */
  162. @Override
  163. protected synchronized ExecutorService getService() {
  164. if (service == null) {
  165. // System.out.println("creating an executor service with a
  166. // threadpool of size " + threadPoolSize);
  167. service = Executors.newFixedThreadPool(threadPoolSize, new ThreadFactory() {
  168. private int count = 0;
  169. @Override
  170. public Thread newThread(Runnable r) {
  171. Thread t = new Thread(r, "tile-pool-" + count++);
  172. t.setPriority(Thread.MIN_PRIORITY);
  173. t.setDaemon(true);
  174. return t;
  175. }
  176. });
  177. }
  178. return service;
  179. }
  180. @Override
  181. public void dispose() {
  182. if (service != null) {
  183. service.shutdown();
  184. service = null;
  185. }
  186. }
  187. /**
  188. * Set the number of threads to use for loading the tiles. This controls the
  189. * number of threads used by the ExecutorService returned from getService().
  190. * Note, this method should be called before loading the first tile. Calls
  191. * after the first tile are loaded will have no effect by default.
  192. *
  193. * @param size
  194. * the thread pool size
  195. */
  196. @Override
  197. public void setThreadPoolSize(int size) {
  198. if (size <= 0) {
  199. throw new IllegalArgumentException(
  200. "size invalid: " + size + ". The size of the threadpool must be greater than 0.");
  201. }
  202. threadPoolSize = size;
  203. }
  204. @Override
  205. protected synchronized void startLoading(Tile tile) {
  206. if (tile.isLoading()) {
  207. // System.out.println("already loading. bailing");
  208. return;
  209. }
  210. tile.setLoading(true);
  211. try {
  212. tileQueue.put(tile);
  213. getService().submit(createTileRunner(tile));
  214. } catch (Exception ex) {
  215. ex.printStackTrace();
  216. }
  217. }
  218. /**
  219. * Increase the priority of this tile so it will be loaded sooner.
  220. *
  221. * @param tile
  222. * the tile
  223. */
  224. public synchronized void promote(CustomTile tile) {
  225. if (tileQueue.contains(tile)) {
  226. try {
  227. tileQueue.remove(tile);
  228. tile.setPriority(Tile.Priority.High);
  229. tileQueue.put(tile);
  230. } catch (Exception ex) {
  231. ex.printStackTrace();
  232. }
  233. }
  234. }
  235. @Override
  236. protected Runnable createTileRunner(Tile tile) {
  237. return new CustomTileRunner();
  238. }
  239. /**
  240. * An inner class which actually loads the tiles. Used by the thread queue.
  241. * Subclasses can override this if necessary.
  242. */
  243. private class CustomTileRunner implements Runnable {
  244. /**
  245. * Gets the full URI of a tile.
  246. *
  247. * @param tile
  248. * the tile
  249. * @throws URISyntaxException
  250. * if the URI is invalid
  251. * @return a URI for the tile
  252. */
  253. protected URI getURI(Tile tile) throws URISyntaxException {
  254. if (tile.getURL() == null) {
  255. return null;
  256. }
  257. return new URI(tile.getURL());
  258. }
  259. /**
  260. * implementation of the Runnable interface.
  261. */
  262. @Override
  263. public void run() {
  264. /*
  265. * 3 strikes and you're out. Attempt to load the url. If it fails,
  266. * decrement the number of tries left and try again. Log failures.
  267. * If I run out of try s just get out. This way, if there is some
  268. * kind of serious failure, I can get out and let other tiles try to
  269. * load.
  270. */
  271. final CustomTile tile = (CustomTile) tileQueue.remove();
  272. int trys = 3;
  273. while (!tile.isLoaded() && trys >= 0) {
  274. try {
  275. BufferedImage img;
  276. URI uri = getURI(tile);
  277. img = cache.get(uri);
  278. if (img == null) {
  279. // put image in chache when loaded
  280. byte[] bimg = cacheInputStream(uri.toURL());
  281. img = ImageIO.read(new ByteArrayInputStream(bimg));
  282. cache.put(uri, bimg, img);
  283. img = cache.get(uri);
  284. }
  285. if (img != null) {
  286. // set image to tile when loaded
  287. final BufferedImage i = img;
  288. SwingUtilities.invokeAndWait(new Runnable() {
  289. @Override
  290. public void run() {
  291. tile.image = new SoftReference<BufferedImage>(i);
  292. tile.setLoaded(true);
  293. tile.setLoading(false);
  294. fireTileLoadedEvent(tile);
  295. }
  296. });
  297. } else {
  298. // something has not been loaded correctly
  299. trys--;
  300. }
  301. } catch (OutOfMemoryError memErr) {
  302. // Cache out of memory order more
  303. cache.needMoreMemory();
  304. } catch (Throwable e) {
  305. // tile could not be loaded
  306. if (trys == 0) {
  307. if (!tileQueue.contains(tile)) {
  308. tileQueue.add(tile);
  309. }
  310. } else {
  311. trys--;
  312. }
  313. }
  314. }
  315. }
  316. // fetch data from OpenStreetMap server
  317. private byte[] cacheInputStream(URL url) throws IOException {
  318. URLConnection connection = url.openConnection();
  319. connection.setRequestProperty("User-Agent", userAgent);
  320. InputStream ins = connection.getInputStream();
  321. ByteArrayOutputStream bout = new ByteArrayOutputStream();
  322. byte[] buf = new byte[256];
  323. while (true) {
  324. int n = ins.read(buf);
  325. if (n == -1)
  326. break;
  327. bout.write(buf, 0, n);
  328. }
  329. ins.close();
  330. byte[] data = bout.toByteArray();
  331. bout.close();
  332. return data;
  333. }
  334. }
  335. }