Ticket #11255: wms_jcs_cache.patch
| File wms_jcs_cache.patch, 285.8 KB (added by , 11 years ago) |
|---|
-
build.xml
diff --git a/build.xml b/build.xml index 04febc6..936e8dd 100644
a b Build-Date: ${build.tstamp} 198 198 <arg value="${mapcss.dir}/MapCSSParser.jj"/> 199 199 </exec> 200 200 </target> 201 <target name="-jaxb_win" if="isWindows"> 202 <property name="xjc" value="${java.home}\..\bin\xjc.exe" /> 203 </target> 204 <target name="-jaxb_nix" unless="isWindows"> 205 <property name="xjc" value="${java.home}/../bin/xjc" /> 206 </target> 207 <target name="jaxb" depends="init, -jaxb_win, -jaxb_nix" unless="jaxb.notRequired"> 208 <exec executable="${xjc}" failonerror="true"> 209 <arg value="-d"/> 210 <arg value="${src.dir}"/> 211 <arg value="-encoding"/> 212 <arg value="UTF-8"/> 213 <arg value="data_nodist/wms-cache.xsd"/> 214 </exec> 215 </target> 216 <target name="compile" depends="init,javacc,jaxb"> 201 <target name="compile" depends="init,javacc"> 217 202 <!-- COTS --> 218 203 <javac srcdir="${src.dir}" includes="com/**,oauth/**,org/apache/commons/**,org/glassfish/**" nowarn="on" encoding="iso-8859-1" 219 204 destdir="build" target="1.7" source="1.7" debug="on" includeAntRuntime="false" createMissingPackageInfoClass="false"> -
deleted file data_nodist/wms-cache.xsd
diff --git a/data_nodist/wms-cache.xsd b/data_nodist/wms-cache.xsd deleted file mode 100644 index c6d50b8..0000000
+ - 1 <?xml version="1.0" encoding="UTF-8"?>2 <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://josm.openstreetmap.de/wms-cache-1.0"3 xmlns:tns="http://josm.openstreetmap.de/wms-cache-1.0" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"5 xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd"6 elementFormDefault="qualified" jaxb:version="2.0">7 8 <annotation>9 <appinfo>10 <jaxb:schemaBindings>11 <jaxb:package name="org.openstreetmap.josm.data.imagery.types">12 </jaxb:package>13 <jaxb:nameXmlTransform>14 <jaxb:typeName suffix="Type" />15 <jaxb:elementName suffix="Type" />16 </jaxb:nameXmlTransform>17 </jaxb:schemaBindings>18 <jaxb:globalBindings>19 <jaxb:javaType name="java.util.Calendar" xmlType="date"20 parseMethod="javax.xml.bind.DatatypeConverter.parseDate"21 printMethod="org.openstreetmap.josm.data.imagery.WmsCache.printDate" />22 </jaxb:globalBindings>23 </appinfo>24 </annotation>25 26 <element name="wms-cache">27 <complexType>28 <sequence>29 <element name="projection" type="tns:projection" minOccurs="0"30 maxOccurs="unbounded" />31 </sequence>32 <attribute name="tileSize" type="int" use="required" />33 <attribute name="totalFileSize" type="int" use="required"/>34 </complexType>35 </element>36 37 <complexType name="projection">38 <sequence>39 <element name="entry" type="tns:entry" minOccurs="0" maxOccurs="unbounded"/>40 </sequence>41 <attribute name="name" type="string"/>42 <attribute name="cache-directory" type="string"/>43 </complexType>44 45 <complexType name="entry">46 <sequence>47 <element name="pixelPerDegree" type="double" />48 <element name="east" type="double" />49 <element name="north" type="double" />50 <element name="lastUsed" type="date" />51 <element name="lastModified" type="date" />52 <element name="filename" type="string" />53 </sequence>54 </complexType>55 </schema> -
new file src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java
diff --git a/src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java b/src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java new file mode 100644 index 0000000..6c7e01a
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.gui.jmapviewer.interfaces; 3 4 import java.util.Map; 5 6 /** 7 * Interface for template tile sources, @see TemplatedTMSTileSource 8 * 9 * @author Wiktor Niesiobędzki 10 * @since TODO 11 */ 12 public interface TemplatedTileSource extends TileSource { 13 /** 14 * 15 * @return headers to be sent with http requests 16 */ 17 public Map<String, String> getHeaders(); 18 } -
src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java index 9c2518f..29d82db 100644
a b import java.util.Map; 8 8 import java.util.Map.Entry; 9 9 10 10 import org.openstreetmap.gui.jmapviewer.OsmMercator; 11 11 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 12 import org.openstreetmap.josm.data.imagery.ImageryInfo; 13 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 14 15 /** 16 * Class generalizing all tile based tile sources 17 * 18 * @author Wiktor Niesiobędzki 19 * 20 */ 12 21 public abstract class AbstractTMSTileSource extends AbstractTileSource { 13 22 14 23 protected String name; … … public abstract class AbstractTMSTileSource extends AbstractTileSource { 19 28 protected int tileSize; 20 29 protected OsmMercator osmMercator; 21 30 31 /** 32 * Creates an instance based on TileSource information 33 * 34 * @param info description of the Tile Source 35 */ 22 36 public AbstractTMSTileSource(TileSourceInfo info) { 23 37 this.name = info.getName(); 24 38 this.baseUrl = info.getUrl(); … … public abstract class AbstractTMSTileSource extends AbstractTileSource { 32 46 osmMercator = new OsmMercator(this.tileSize); 33 47 } 34 48 49 /** 50 * @return default tile size to use, when not set in Imagery Preferences 51 */ 52 protected int getDefaultTileSize() { 53 return OsmMercator.DEFAUL_TILE_SIZE; 54 } 55 35 56 @Override 36 57 public String getName() { 37 58 return name; … … public abstract class AbstractTMSTileSource extends AbstractTileSource { 52 73 return 0; 53 74 } 54 75 76 /** 77 * @return image extension, used for URL creation 78 */ 55 79 public String getExtension() { 56 80 return "png"; 57 81 } 58 82 59 83 /** 84 * @param zoom level of the tile 85 * @param tilex tile number in x axis 86 * @param tiley tile number in y axis 87 * @return String containg path part of URL of the tile 60 88 * @throws IOException when subclass cannot return the tile URL 61 89 */ 62 90 public String getTilePath(int zoom, int tilex, int tiley) throws IOException { 63 91 return "/" + zoom + "/" + tilex + "/" + tiley + "." + getExtension(); 64 92 } 65 93 94 /** 95 * @return Base part of the URL of the tile source 96 */ 66 97 public String getBaseUrl() { 67 98 return this.baseUrl; 68 99 } … … public abstract class AbstractTMSTileSource extends AbstractTileSource { 87 118 */ 88 119 @Override 89 120 public int getTileSize() { 121 if (tileSize <= 0) { 122 return getDefaultTileSize(); 123 }; 90 124 return tileSize; 91 125 } 92 126 … … public abstract class AbstractTMSTileSource extends AbstractTileSource { 152 186 return super.isNoTileAtZoom(headers, statusCode, content); 153 187 } 154 188 189 /** 190 * Converts imagery info to any of TMS supported TileSource 191 * @param info 192 * @return TileSource for specified info 193 * @throws IllegalArgumentException 194 */ 195 public static TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException { 196 if (info.getImageryType() == ImageryType.TMS) { 197 TemplatedTMSTileSource.checkUrl(info.getUrl()); 198 TMSTileSource t = new TemplatedTMSTileSource(info); 199 info.setAttribution(t); 200 return t; 201 } else if (info.getImageryType() == ImageryType.BING) 202 return new CachedAttributionBingAerialTileSource(info); 203 else if (info.getImageryType() == ImageryType.SCANEX) { 204 return new ScanexTileSource(info); 205 } 206 return null; 207 } 208 155 209 @Override 156 210 public Map<String, String> getMetadata(Map<String, List<String>> headers) { 157 211 Map<String, String> ret = new HashMap<>(); -
new file src/org/openstreetmap/gui/jmapviewer/tilesources/CachedAttributionBingAerialTileSource.java
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/CachedAttributionBingAerialTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/CachedAttributionBingAerialTileSource.java new file mode 100644 index 0000000..42f9cf4
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.gui.jmapviewer.tilesources; 3 4 import java.io.IOException; 5 import java.io.StringReader; 6 import java.net.URL; 7 import java.util.List; 8 import java.util.Scanner; 9 import java.util.concurrent.Callable; 10 11 import org.openstreetmap.josm.Main; 12 import org.openstreetmap.josm.data.imagery.ImageryInfo; 13 import org.openstreetmap.josm.io.CacheCustomContent; 14 import org.openstreetmap.josm.io.UTFInputStreamReader; 15 import org.openstreetmap.josm.tools.Utils; 16 import org.xml.sax.InputSource; 17 18 /** 19 * Bing TileSource with cached attribution 20 * 21 * @author Wiktor Niesiobędzki 22 * @since TODO 23 * 24 */ 25 public class CachedAttributionBingAerialTileSource extends BingAerialTileSource { 26 /** 27 * Creates tile source 28 * @param info ImageryInfo description of this tile source 29 */ 30 public CachedAttributionBingAerialTileSource(ImageryInfo info) { 31 super(info); 32 } 33 34 class BingAttributionData extends CacheCustomContent<IOException> { 35 36 public BingAttributionData() { 37 super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY); 38 } 39 40 @Override 41 protected byte[] updateData() throws IOException { 42 URL u = getAttributionUrl(); 43 try (Scanner scanner = new Scanner(UTFInputStreamReader.create(Utils.openURL(u)))) { 44 String r = scanner.useDelimiter("\\A").next(); 45 Main.info("Successfully loaded Bing attribution data."); 46 return r.getBytes("UTF-8"); 47 } 48 } 49 } 50 51 @Override 52 protected Callable<List<Attribution>> getAttributionLoaderCallable() { 53 return new Callable<List<Attribution>>() { 54 55 @Override 56 public List<Attribution> call() throws Exception { 57 BingAttributionData attributionLoader = new BingAttributionData(); 58 int waitTimeSec = 1; 59 while (true) { 60 try { 61 String xml = attributionLoader.updateIfRequiredString(); 62 return parseAttributionText(new InputSource(new StringReader((xml)))); 63 } catch (IOException ex) { 64 Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds."); 65 Thread.sleep(waitTimeSec * 1000L); 66 waitTimeSec *= 2; 67 } 68 } 69 } 70 }; 71 } 72 } 73 -
src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java index 4b43ff0..1e18c48 100644
a b 1 1 // License: GPL. For details, see Readme.txt file. 2 2 package org.openstreetmap.gui.jmapviewer.tilesources; 3 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 4 6 import java.util.HashMap; 5 7 import java.util.Map; 6 8 import java.util.Random; 7 9 import java.util.regex.Matcher; 8 10 import java.util.regex.Pattern; 9 11 10 public class TemplatedTMSTileSource extends TMSTileSource { 12 import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; 13 import org.openstreetmap.josm.data.imagery.ImageryInfo; 14 import org.openstreetmap.josm.tools.CheckParameterUtil; 15 16 /** 17 * Handles templated TMS Tile Source. Templated means, that some patterns within 18 * URL gets substituted. 19 * 20 * Supported parameters 21 * {zoom} - substituted with zoom level 22 * {z} - as above 23 * {NUMBER-zoom} - substituted with result of equation "NUMBER - zoom", 24 * eg. {20-zoom} for zoom level 15 will result in 5 in this place 25 * {zoom+number} - substituted with result of equation "zoom + number", 26 * eg. {zoom+5} for zoom level 15 will result in 20. 27 * {x} - substituted with X tile number 28 * {y} - substituted with Y tile number 29 * {!y} - substituted with Yahoo Y tile number 30 * {-y} - substituted with reversed Y tile number 31 * {switch:VAL_A,VAL_B,VAL_C,...} - substituted with one of VAL_A, VAL_B, VAL_C. Usually 32 * used to specify many tile servers 33 * {header:(HEADER_NAME,HEADER_VALUE)} - sets the headers to be sent to tile server 34 */ 35 36 public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTileSource { 11 37 12 38 private Random rand = null; 13 39 private String[] randomParts = null; 14 40 private Map<String, String> headers = new HashMap<>(); 15 41 16 p ublicstatic final String COOKIE_HEADER = "Cookie";17 p ublicstatic final String PATTERN_ZOOM = "\\{(?:(\\d+)-)?z(?:oom)?([+-]\\d+)?\\}";18 p ublicstatic final String PATTERN_X = "\\{x\\}";19 p ublicstatic final String PATTERN_Y = "\\{y\\}";20 p ublicstatic final String PATTERN_Y_YAHOO = "\\{!y\\}";21 p ublicstatic final String PATTERN_NEG_Y = "\\{-y\\}";22 p ublicstatic final String PATTERN_SWITCH = "\\{switch:([^}]+)\\}";23 p ublicstatic final String PATTERN_HEADER = "\\{header\\(([^,]+),([^}]+)\\)\\}";42 private static final String COOKIE_HEADER = "Cookie"; 43 private static final String PATTERN_ZOOM = "\\{(?:(\\d+)-)?z(?:oom)?([+-]\\d+)?\\}"; 44 private static final String PATTERN_X = "\\{x\\}"; 45 private static final String PATTERN_Y = "\\{y\\}"; 46 private static final String PATTERN_Y_YAHOO = "\\{!y\\}"; 47 private static final String PATTERN_NEG_Y = "\\{-y\\}"; 48 private static final String PATTERN_SWITCH = "\\{switch:([^}]+)\\}"; 49 private static final String PATTERN_HEADER = "\\{header\\(([^,]+),([^}]+)\\)\\}"; 24 50 25 p ublicstatic final String[] ALL_PATTERNS = {51 private static final String[] ALL_PATTERNS = { 26 52 PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y, 27 53 PATTERN_SWITCH 28 54 }; 29 55 30 public TemplatedTMSTileSource(TileSourceInfo info) { 56 /** 57 * Creates Templated TMS Tile Source based on ImageryInfo 58 * @param info 59 */ 60 public TemplatedTMSTileSource(ImageryInfo info) { 31 61 super(info); 32 62 if (info.getCookies() != null) { 33 63 headers.put(COOKIE_HEADER, info.getCookies()); … … public class TemplatedTMSTileSource extends TMSTileSource { 83 113 } 84 114 return r; 85 115 } 116 117 /** 118 * Checks if url is acceptable by this Tile Source 119 * @param url 120 */ 121 public static void checkUrl(String url) { 122 CheckParameterUtil.ensureParameterNotNull(url, "url"); 123 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url); 124 while (m.find()) { 125 boolean isSupportedPattern = false; 126 for (String pattern : ALL_PATTERNS) { 127 if (m.group().matches(pattern)) { 128 isSupportedPattern = true; 129 break; 130 } 131 } 132 if (!isSupportedPattern) { 133 throw new IllegalArgumentException( 134 tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url)); 135 } 136 } 137 } 86 138 } -
src/org/openstreetmap/gui/jmapviewer/tilesources/TileSourceInfo.java
diff --git a/src/org/openstreetmap/gui/jmapviewer/tilesources/TileSourceInfo.java b/src/org/openstreetmap/gui/jmapviewer/tilesources/TileSourceInfo.java index 639a9d0..2aa48dd 100644
a b package org.openstreetmap.gui.jmapviewer.tilesources; 3 3 4 4 import java.util.Map; 5 5 6 import org.openstreetmap.gui.jmapviewer.OsmMercator;7 8 6 /** 9 7 * Data class that keeps basic information about a tile source. 10 8 * … … public class TileSourceInfo { 32 30 protected String cookies; 33 31 34 32 /** tile size of the displayed tiles */ 35 private int tileSize = OsmMercator.DEFAUL_TILE_SIZE;33 private int tileSize = -1; // use default 36 34 37 35 /** mapping <header key, metadata key> */ 38 36 protected Map<String, String> metadataHeaders; … … public class TileSourceInfo { 115 113 116 114 /** 117 115 * Request tile size of this tile source 118 * @return tile size provided by this tile source 116 * @return tile size provided by this tile source, or -1 when default value should be used 119 117 */ 120 118 public int getTileSize() { 121 119 return tileSize; … … public class TileSourceInfo { 126 124 * @param tileSize 127 125 */ 128 126 public void setTileSize(int tileSize) { 129 if (tileSize <= 0) {127 if (tileSize == 0 || tileSize < -1) { 130 128 throw new AssertionError("Invalid tile size: " + tileSize); 131 129 } 132 130 this.tileSize = tileSize; -
src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
diff --git a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java index f494675..d28251c 100644
a b public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements 189 189 return; 190 190 } 191 191 // object not in cache, so submit work to separate thread 192 getDownloadExecutor().execute(this);192 downloadJobExecutor.execute(this); 193 193 } 194 194 } 195 195 … … public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements 226 226 return getUrl().getHost(); 227 227 } 228 228 229 /**230 * this needs to be non-static, so it can be overridden by subclasses231 */232 protected ThreadPoolExecutor getDownloadExecutor() {233 return downloadJobExecutor;234 }235 236 229 public void run() { 237 230 final Thread currentThread = Thread.currentThread(); 238 231 final String oldName = currentThread.getName(); … … public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements 466 459 * cancels all outstanding tasks in the queue. 467 460 */ 468 461 public void cancelOutstandingTasks() { 469 ThreadPoolExecutor downloadExecutor = getDownloadExecutor(); 470 for(Runnable r: downloadExecutor.getQueue()) { 471 if (downloadExecutor.remove(r)) { 472 if (r instanceof JCSCachedTileLoaderJob) { 462 for(Runnable r: downloadJobExecutor.getQueue()) { 463 if (downloadJobExecutor.remove(r) && r instanceof JCSCachedTileLoaderJob) { 473 464 ((JCSCachedTileLoaderJob<?, ?>) r).handleJobCancellation(); 474 465 } 475 466 } 476 467 } 477 }478 468 479 469 /** 480 470 * Sets a job, that will be run, when job will finish execution -
new file src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java
diff --git a/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java b/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java new file mode 100644 index 0000000..24c04da
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery; 3 4 import java.io.File; 5 import java.io.IOException; 6 import java.util.HashMap; 7 import java.util.Map; 8 9 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 10 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 11 import org.openstreetmap.josm.Main; 12 import org.openstreetmap.josm.data.Version; 13 import org.openstreetmap.josm.data.preferences.StringProperty; 14 15 /** 16 * TileLoaderFactory creating JCS cached TileLoaders 17 * 18 * @author Wiktor Niesiobędzki 19 * @since TODO 20 * 21 */ 22 public abstract class CachedTileLoaderFactory implements TileLoaderFactory { 23 /** 24 * Keeps the cache directory where 25 */ 26 public static final StringProperty PROP_TILECACHE_DIR = getTileCacheDir(); 27 private String cacheName; 28 29 /** 30 * @param cacheName name of the cache region, that the created loader will use 31 */ 32 public CachedTileLoaderFactory(String cacheName) { 33 this.cacheName = cacheName; 34 } 35 36 private static StringProperty getTileCacheDir() { 37 String defPath = null; 38 try { 39 defPath = new File(Main.pref.getCacheDirectory(), "tiles").getAbsolutePath(); 40 } catch (SecurityException e) { 41 Main.warn(e); 42 } 43 return new StringProperty("imagery.generic.loader.cachedir", defPath); 44 } 45 46 @Override 47 public TileLoader makeTileLoader(TileLoaderListener listener) { 48 return makeTileLoader(listener, null); 49 } 50 51 @Override 52 public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) { 53 Map<String, String> headers = new HashMap<>(); 54 headers.put("User-Agent", Version.getInstance().getFullAgentString()); 55 headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*"); 56 if (inputHeaders != null) 57 headers.putAll(inputHeaders); 58 59 try { 60 return getLoader(listener, cacheName, 61 Main.pref.getInteger("socket.timeout.connect",15) * 1000, 62 Main.pref.getInteger("socket.timeout.read", 30) * 1000, 63 headers, 64 PROP_TILECACHE_DIR.get()); 65 } catch (IOException e) { 66 Main.warn(e); 67 } 68 return null; 69 } 70 71 protected abstract TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout, int readTimeout, Map<String, String> headers, String cacheDir) throws IOException; 72 } -
deleted file src/org/openstreetmap/josm/data/imagery/GeorefImage.java
diff --git a/src/org/openstreetmap/josm/data/imagery/GeorefImage.java b/src/org/openstreetmap/josm/data/imagery/GeorefImage.java deleted file mode 100644 index 228ece2..0000000
+ - 1 // License: GPL. For details, see LICENSE file.2 package org.openstreetmap.josm.data.imagery;3 4 import static org.openstreetmap.josm.tools.I18n.tr;5 6 import java.awt.Color;7 import java.awt.Font;8 import java.awt.Graphics;9 import java.awt.Image;10 import java.awt.Transparency;11 import java.awt.image.BufferedImage;12 import java.io.IOException;13 import java.io.ObjectInputStream;14 import java.io.ObjectOutputStream;15 import java.io.Serializable;16 import java.lang.ref.SoftReference;17 18 import javax.imageio.ImageIO;19 20 import org.openstreetmap.josm.data.coor.EastNorth;21 import org.openstreetmap.josm.gui.NavigatableComponent;22 import org.openstreetmap.josm.gui.layer.ImageryLayer;23 import org.openstreetmap.josm.gui.layer.WMSLayer;24 import org.openstreetmap.josm.tools.ImageProvider;25 26 public class GeorefImage implements Serializable {27 private static final long serialVersionUID = 1L;28 29 public enum State { IMAGE, NOT_IN_CACHE, FAILED, PARTLY_IN_CACHE}30 31 private WMSLayer layer;32 private State state;33 34 private BufferedImage image;35 private SoftReference<BufferedImage> reImg;36 private int xIndex;37 private int yIndex;38 39 private static final Color transparentColor = new Color(0,0,0,0);40 private Color fadeColor = transparentColor;41 42 public EastNorth getMin() {43 return layer.getEastNorth(xIndex, yIndex);44 }45 46 public EastNorth getMax() {47 return layer.getEastNorth(xIndex+1, yIndex+1);48 }49 50 public GeorefImage(WMSLayer layer) {51 this.layer = layer;52 }53 54 public void changePosition(int xIndex, int yIndex) {55 if (!equalPosition(xIndex, yIndex)) {56 this.xIndex = xIndex;57 this.yIndex = yIndex;58 this.image = null;59 flushResizedCachedInstance();60 }61 }62 63 public boolean equalPosition(int xIndex, int yIndex) {64 return this.xIndex == xIndex && this.yIndex == yIndex;65 }66 67 /**68 * Resets this image to initial state and release all resources being used.69 * @since 713270 */71 public void resetImage() {72 if (image != null) {73 image.flush();74 }75 changeImage(null, null, null);76 }77 78 public void changeImage(State state, BufferedImage image, String errorMsg) {79 flushResizedCachedInstance();80 this.image = image;81 this.state = state;82 if (state == null)83 return;84 switch (state) {85 case FAILED:86 BufferedImage imgFailed = createImage();87 layer.drawErrorTile(imgFailed, errorMsg);88 this.image = imgFailed;89 break;90 case NOT_IN_CACHE:91 BufferedImage img = createImage();92 Graphics g = img.getGraphics();93 g.setColor(Color.GRAY);94 g.fillRect(0, 0, img.getWidth(), img.getHeight());95 Font font = g.getFont();96 Font tempFont = font.deriveFont(Font.PLAIN).deriveFont(36.0f);97 g.setFont(tempFont);98 g.setColor(Color.BLACK);99 String text = tr("Not in cache");100 g.drawString(text, (img.getWidth() - g.getFontMetrics().stringWidth(text)) / 2, img.getHeight()/2);101 g.setFont(font);102 this.image = img;103 break;104 default:105 if (this.image != null) {106 this.image = layer.sharpenImage(this.image);107 }108 break;109 }110 }111 112 private BufferedImage createImage() {113 return new BufferedImage(layer.getImageSize(), layer.getImageSize(), BufferedImage.TYPE_INT_RGB);114 }115 116 public boolean paint(Graphics g, NavigatableComponent nc, int xIndex, int yIndex, int leftEdge, int bottomEdge) {117 if (getImage() == null)118 return false;119 120 if(!(this.xIndex == xIndex && this.yIndex == yIndex))121 return false;122 123 int left = layer.getImageX(xIndex);124 int bottom = layer.getImageY(yIndex);125 int width = layer.getImageWidth(xIndex);126 int height = layer.getImageHeight(yIndex);127 128 int x = left - leftEdge;129 int y = nc.getHeight() - (bottom - bottomEdge) - height;130 131 // This happens if you zoom outside the world132 if(width == 0 || height == 0)133 return false;134 135 // TODO: implement per-layer fade color136 Color newFadeColor;137 if (ImageryLayer.PROP_FADE_AMOUNT.get() == 0) {138 newFadeColor = transparentColor;139 } else {140 newFadeColor = ImageryLayer.getFadeColorWithAlpha();141 }142 143 BufferedImage img = reImg == null?null:reImg.get();144 if(img != null && img.getWidth() == width && img.getHeight() == height && fadeColor.equals(newFadeColor)) {145 g.drawImage(img, x, y, null);146 return true;147 }148 149 fadeColor = newFadeColor;150 151 boolean alphaChannel = WMSLayer.PROP_ALPHA_CHANNEL.get() && getImage().getTransparency() != Transparency.OPAQUE;152 153 try {154 if(img != null) {155 img.flush();156 }157 long freeMem = Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory();158 // Notice that this value can get negative due to integer overflows159 160 int multipl = alphaChannel ? 4 : 3;161 // This happens when requesting images while zoomed out and then zooming in162 // Storing images this large in memory will certainly hang up JOSM. Luckily163 // traditional rendering is as fast at these zoom levels, so it's no loss.164 // Also prevent caching if we're out of memory soon165 if(width > 2000 || height > 2000 || width*height*multipl > freeMem) {166 fallbackDraw(g, getImage(), x, y, width, height, alphaChannel);167 } else {168 // We haven't got a saved resized copy, so resize and cache it169 img = new BufferedImage(width, height, alphaChannel?BufferedImage.TYPE_INT_ARGB:BufferedImage.TYPE_3BYTE_BGR);170 img.getGraphics().drawImage(getImage(),171 0, 0, width, height, // dest172 0, 0, getImage().getWidth(null), getImage().getHeight(null), // src173 null);174 if (!alphaChannel) {175 drawFadeRect(img.getGraphics(), 0, 0, width, height);176 }177 img.getGraphics().dispose();178 g.drawImage(img, x, y, null);179 reImg = new SoftReference<>(img);180 }181 } catch(Exception e) {182 fallbackDraw(g, getImage(), x, y, width, height, alphaChannel);183 }184 return true;185 }186 187 private void fallbackDraw(Graphics g, Image img, int x, int y, int width, int height, boolean alphaChannel) {188 flushResizedCachedInstance();189 g.drawImage(190 img, x, y, x + width, y + height,191 0, 0, img.getWidth(null), img.getHeight(null),192 null);193 if (!alphaChannel) { //FIXME: fading for layers with alpha channel currently is not supported194 drawFadeRect(g, x, y, width, height);195 }196 }197 198 private void drawFadeRect(Graphics g, int x, int y, int width, int height) {199 if (fadeColor != transparentColor) {200 g.setColor(fadeColor);201 g.fillRect(x, y, width, height);202 }203 }204 205 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {206 state = (State) in.readObject();207 boolean hasImage = in.readBoolean();208 if (hasImage) {209 image = ImageProvider.read(ImageIO.createImageInputStream(in), true, WMSLayer.PROP_ALPHA_CHANNEL.get());210 } else {211 in.readObject(); // read null from input stream212 image = null;213 }214 }215 216 private void writeObject(ObjectOutputStream out) throws IOException {217 out.writeObject(state);218 if(getImage() == null) {219 out.writeBoolean(false);220 out.writeObject(null);221 } else {222 out.writeBoolean(true);223 ImageIO.write(getImage(), "png", ImageIO.createImageOutputStream(out));224 }225 }226 227 public void flushResizedCachedInstance() {228 if (reImg != null) {229 BufferedImage img = reImg.get();230 if (img != null) {231 img.flush();232 }233 }234 reImg = null;235 }236 237 public BufferedImage getImage() {238 return image;239 }240 241 public State getState() {242 return state;243 }244 245 public int getXIndex() {246 return xIndex;247 }248 249 public int getYIndex() {250 return yIndex;251 }252 253 public void setLayer(WMSLayer layer) {254 this.layer = layer;255 }256 } -
src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java
diff --git a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java index b8d92c1..e63ae0f 100644
a b import org.openstreetmap.josm.data.preferences.IntegerProperty; 28 28 */ 29 29 public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCache { 30 30 31 pr ivateICacheAccess<String, BufferedImageCacheEntry> cache;32 pr ivateint connectTimeout;33 pr ivateint readTimeout;34 pr ivateMap<String, String> headers;35 pr ivateTileLoaderListener listener;31 protected final ICacheAccess<String, BufferedImageCacheEntry> cache; 32 protected final int connectTimeout; 33 protected final int readTimeout; 34 protected final Map<String, String> headers; 35 protected final TileLoaderListener listener; 36 36 private static final String PREFERENCE_PREFIX = "imagery.tms.cache."; 37 37 /** 38 38 * how many object on disk should be stored for TMS region. Average tile size is about 20kb 39 * 40 * 25000 is around 500MB under this assumptions 39 41 */ 40 public static final IntegerProperty MAX_OBJECTS_ON_DISK = new IntegerProperty(PREFERENCE_PREFIX + "max_objects_disk", 25000); // 25000 is around 500MB under this assumptions42 public static final IntegerProperty MAX_OBJECTS_ON_DISK = new IntegerProperty(PREFERENCE_PREFIX + "max_objects_disk", 25000); 41 43 42 44 /** 43 45 * overrides the THREAD_LIMIT in superclass, as we want to have separate limit and pool for TMS … … public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCa 54 56 * separate from JCS thread pool for TMS loader, so we can have different thread pools for default JCS 55 57 * and for TMS imagery 56 58 */ 57 private static ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = getThreadPoolExecutor(); 58 59 private static ThreadPoolExecutor getThreadPoolExecutor() { 60 return new ThreadPoolExecutor( 61 THREAD_LIMIT.get().intValue(), // keep the thread number constant 62 THREAD_LIMIT.get().intValue(), // do not this number of threads 63 30, // keepalive for thread 64 TimeUnit.SECONDS, 65 new HostLimitQueue(HOST_LIMIT.get().intValue()), 66 JCSCachedTileLoaderJob.getNamedThreadFactory("TMS downloader") 67 ); 68 } 59 private static ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = getNewThreadPoolExecutor("TMS downloader"); 69 60 70 61 private ThreadPoolExecutor downloadExecutor = DEFAULT_DOWNLOAD_JOB_DISPATCHER; 71 62 … … public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCa 90 81 this.listener = listener; 91 82 } 92 83 84 /** 85 * @param name name of the threads 86 * @param workers number of worker thread to keep 87 * @return new ThreadPoolExecutor that will use a @see HostLimitQueue based queue 88 */ 89 public static ThreadPoolExecutor getNewThreadPoolExecutor(String name, int workers) { 90 return new ThreadPoolExecutor( 91 workers, // keep the thread number constant 92 workers, // do not this number of threads 93 30, // keepalive for thread 94 TimeUnit.SECONDS, 95 new HostLimitQueue(HOST_LIMIT.get().intValue()), 96 JCSCachedTileLoaderJob.getNamedThreadFactory(name) 97 ); 98 } 99 100 /** 101 * @param name name of threads 102 * @return new ThreadPoolExecutor that will use a @see HostLimitQueue based queue, with default number of threads 103 */ 104 public static ThreadPoolExecutor getNewThreadPoolExecutor(String name) { 105 return getNewThreadPoolExecutor(name, THREAD_LIMIT.get().intValue()); 106 } 107 93 108 @Override 94 109 public TileJob createTileLoaderJob(Tile tile) { 95 110 return new TMSCachedTileLoaderJob(listener, tile, cache, 96 connectTimeout, readTimeout, headers, downloadExecutor);111 connectTimeout, readTimeout, headers, getDownloadExecutor()); 97 112 } 98 113 99 114 @Override … … public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCa 139 154 } 140 155 } 141 156 } 157 158 /** 159 * Sets the download executor that will be used to download tiles instead of default one. 160 * You can use {@link #getNewThreadPoolExecutor} to create a new download executor with separate 161 * queue from default. 162 * 163 * @param downloadExecutor 164 */ 165 public void setDownloadExecutor(ThreadPoolExecutor downloadExecutor) { 166 this.downloadExecutor = downloadExecutor; 167 } 168 169 /** 170 * @return download executor that is used by this factory 171 */ 172 public ThreadPoolExecutor getDownloadExecutor() { 173 return downloadExecutor; 174 } 175 142 176 } -
new file src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java
diff --git a/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java b/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java new file mode 100644 index 0000000..3d13b31
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.io.IOException; 7 import java.text.DecimalFormat; 8 import java.text.DecimalFormatSymbols; 9 import java.text.NumberFormat; 10 import java.util.HashMap; 11 import java.util.Locale; 12 import java.util.Map; 13 import java.util.regex.Matcher; 14 import java.util.regex.Pattern; 15 16 import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; 17 import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource; 18 import org.openstreetmap.josm.Main; 19 import org.openstreetmap.josm.data.coor.EastNorth; 20 import org.openstreetmap.josm.data.coor.LatLon; 21 import org.openstreetmap.josm.data.projection.Projection; 22 import org.openstreetmap.josm.gui.layer.WMSLayer; 23 import org.openstreetmap.josm.tools.CheckParameterUtil; 24 25 /** 26 * Tile Source handling WMS providers 27 * 28 * @author Wiktor Niesiobędzki 29 * @since TODO 30 * 31 */ 32 public class TemplatedWMSTileSource extends TMSTileSource implements TemplatedTileSource { 33 private Map<String, String> headers = new HashMap<>(); 34 private ImageryInfo info; 35 private static final String COOKIE_HEADER = "Cookie"; 36 private static final String PATTERN_HEADER = "\\{header\\(([^,]+),([^}]+)\\)\\}"; 37 private static final String PATTERN_PROJ = "\\{proj(\\([^})]+\\))?\\}"; 38 private static final String PATTERN_BBOX = "\\{bbox\\}"; 39 private static final String PATTERN_W = "\\{w\\}"; 40 private static final String PATTERN_S = "\\{s\\}"; 41 private static final String PATTERN_E = "\\{e\\}"; 42 private static final String PATTERN_N = "\\{n\\}"; 43 private static final String PATTERN_WIDTH = "\\{width\\}"; 44 private static final String PATTERN_HEIGHT = "\\{height\\}"; 45 46 47 private static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US)); 48 49 private static final String[] ALL_PATTERNS = { 50 PATTERN_HEADER, PATTERN_PROJ, PATTERN_BBOX, PATTERN_W, PATTERN_S, PATTERN_E, PATTERN_N, PATTERN_WIDTH, PATTERN_HEIGHT 51 }; 52 53 /** 54 * Creates a tile source based on imagery info 55 * @param info 56 */ 57 public TemplatedWMSTileSource(ImageryInfo info) { 58 super(info); 59 this.info = info; 60 if (info.getCookies() != null) { 61 headers.put(COOKIE_HEADER, info.getCookies()); 62 } 63 64 handleTemplate(); 65 } 66 67 private void handleTemplate() { 68 // Capturing group pattern on switch values 69 Pattern pattern = Pattern.compile(PATTERN_HEADER); 70 StringBuffer output = new StringBuffer(); 71 Matcher matcher = pattern.matcher(this.baseUrl); 72 while (matcher.find()) { 73 headers.put(matcher.group(1),matcher.group(2)); 74 matcher.appendReplacement(output, ""); 75 } 76 matcher.appendTail(output); 77 this.baseUrl = output.toString(); 78 } 79 80 @Override 81 protected int getDefaultTileSize() { 82 return WMSLayer.PROP_IMAGE_SIZE.get(); 83 } 84 85 @Override 86 public String getTileUrl(int zoom, int tilex, int tiley) throws IOException { 87 Projection myProj = Main.getProjection(); 88 String myProjCode = Main.getProjection().toCode(); 89 90 EastNorth sw = myProj.latlon2eastNorth(new LatLon( 91 tileYToLat(tiley+1, zoom), 92 tileXToLon(tilex, zoom) 93 )); 94 EastNorth ne = myProj.latlon2eastNorth(new LatLon( 95 tileYToLat(tiley, zoom), 96 tileXToLon(tilex+1, zoom) 97 )); 98 99 double w = sw.getX(); 100 double s = sw.getY(); 101 102 double e = ne.getX(); 103 double n = ne.getY(); 104 105 if (!info.getServerProjections().contains(myProjCode) && "EPSG:3857".equals(Main.getProjection().toCode())) { 106 LatLon swll = Main.getProjection().eastNorth2latlon(new EastNorth(w, s)); 107 LatLon nell = Main.getProjection().eastNorth2latlon(new EastNorth(e, n)); 108 myProjCode = "EPSG:4326"; 109 s = swll.lat(); 110 w = swll.lon(); 111 n = nell.lat(); 112 e = nell.lon(); 113 } 114 if ("EPSG:4326".equals(myProjCode) && !info.getServerProjections().contains(myProjCode) && info.getServerProjections().contains("CRS:84")) { 115 myProjCode = "CRS:84"; 116 } 117 118 // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326. 119 // 120 // Background: 121 // 122 // bbox=x_min,y_min,x_max,y_max 123 // 124 // SRS=... is WMS 1.1.1 125 // CRS=... is WMS 1.3.0 126 // 127 // The difference: 128 // For SRS x is east-west and y is north-south 129 // For CRS x and y are as specified by the EPSG 130 // E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326. 131 // For most other EPSG code there seems to be no difference. 132 // [1] https://www.epsg-registry.org/report.htm?type=selection&entity=urn:ogc:def:crs:EPSG::4326&reportDetail=short&style=urn:uuid:report-style:default-with-code&style_name=OGP%20Default%20With%20Code&title=EPSG:4326 133 boolean switchLatLon = false; 134 if (baseUrl.toLowerCase().contains("crs=epsg:4326")) { 135 switchLatLon = true; 136 } else if (baseUrl.toLowerCase().contains("crs=") && "EPSG:4326".equals(myProjCode)) { 137 switchLatLon = true; 138 } 139 String bbox; 140 if (switchLatLon) { 141 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e)); 142 } else { 143 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n)); 144 } 145 return baseUrl. 146 replaceAll(PATTERN_PROJ, myProjCode) 147 .replaceAll(PATTERN_BBOX, bbox) 148 .replaceAll(PATTERN_W, latLonFormat.format(w)) 149 .replaceAll(PATTERN_S, latLonFormat.format(s)) 150 .replaceAll(PATTERN_E, latLonFormat.format(e)) 151 .replaceAll(PATTERN_N, latLonFormat.format(n)) 152 .replaceAll(PATTERN_WIDTH, String.valueOf(getTileSize())) 153 .replaceAll(PATTERN_HEIGHT, String.valueOf(getTileSize())) 154 .replace(" ", "%20"); 155 } 156 157 /** 158 * Checks if url is acceptable by this Tile Source 159 * @param url 160 */ 161 public static void checkUrl(String url) { 162 CheckParameterUtil.ensureParameterNotNull(url, "url"); 163 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url); 164 while (m.find()) { 165 boolean isSupportedPattern = false; 166 for (String pattern : ALL_PATTERNS) { 167 if (m.group().matches(pattern)) { 168 isSupportedPattern = true; 169 break; 170 } 171 } 172 if (!isSupportedPattern) { 173 throw new IllegalArgumentException( 174 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url)); 175 } 176 } 177 } 178 179 @Override 180 public Map<String, String> getHeaders() { 181 return headers; 182 } 183 } -
new file src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java
diff --git a/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java b/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java new file mode 100644 index 0000000..2c1dd82
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery; 3 4 import java.util.Map; 5 6 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 7 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 8 9 /** 10 * Factory creating TileLoaders for layers 11 * 12 * @author Wiktor Niesiobędzki 13 * @since TODO 14 * 15 */ 16 public interface TileLoaderFactory { 17 18 /** 19 * @param listener that will be notified, when tile has finished loading 20 * @return TileLoader that notifies specified listener 21 */ 22 TileLoader makeTileLoader(TileLoaderListener listener); 23 24 /** 25 * @param listener that will be notified, when tile has finished loading 26 * @param headers that will be sent with requests to TileSource 27 * @return TileLoader that uses both of above 28 */ 29 TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers); 30 31 } -
new file src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java
diff --git a/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java new file mode 100644 index 0000000..45b0970
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery; 3 4 import java.io.IOException; 5 import java.util.Map; 6 7 import org.openstreetmap.gui.jmapviewer.Tile; 8 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 9 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 10 import org.openstreetmap.josm.data.preferences.IntegerProperty; 11 12 /** 13 * Tileloader for WMS based imagery. It is separate to use different ThreadPoolExecutor, as we want 14 * to define number of simultaneous downloads for WMS separately 15 * 16 * @author Wiktor Niesiobędzki 17 * @since TODO 18 * 19 */ 20 public class WMSCachedTileLoader extends TMSCachedTileLoader { 21 22 /** limit of concurrent connections to WMS tile source (per source) */ 23 public static IntegerProperty THREAD_LIMIT = new IntegerProperty("imagery.wms.simultaneousConnections", 3); 24 25 /** 26 * Creates a TileLoader with separate WMS downloader. 27 * 28 * @param listener that will be notified when tile is loaded 29 * @param name name of the cache region 30 * @param connectTimeout to tile source 31 * @param readTimeout from tile source 32 * @param headers to be sent with requests 33 * @param cacheDir place to store the cache 34 * @throws IOException when there is a problem creating cache repository 35 */ 36 public WMSCachedTileLoader(TileLoaderListener listener, String name, int connectTimeout, int readTimeout, 37 Map<String, String> headers, String cacheDir) throws IOException { 38 39 super(listener, name, connectTimeout, readTimeout, headers, cacheDir); 40 setDownloadExecutor(TMSCachedTileLoader.getNewThreadPoolExecutor("WMS downloader", THREAD_LIMIT.get())); 41 } 42 43 @Override 44 public TileJob createTileLoaderJob(Tile tile) { 45 return new WMSCachedTileLoaderJob(listener, tile, cache, connectTimeout, readTimeout, headers, getDownloadExecutor()); 46 } 47 } -
new file src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java
diff --git a/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java new file mode 100644 index 0000000..c9f5356
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery; 3 4 import java.util.List; 5 import java.util.Map; 6 import java.util.concurrent.ThreadPoolExecutor; 7 8 import org.apache.commons.jcs.access.behavior.ICacheAccess; 9 import org.openstreetmap.gui.jmapviewer.Tile; 10 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 11 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 12 13 /** 14 * Separate class to handle WMS jobs, as it needs to react differently to HTTP response codes from WMS server 15 * 16 * @author Wiktor Niesiobędzki 17 * @since TODO 18 * 19 */ 20 public class WMSCachedTileLoaderJob extends TMSCachedTileLoaderJob { 21 22 /** 23 * Creates a job - that will download specific tile 24 * @param listener will be notified, when tile has loaded 25 * @param tile to load 26 * @param cache to use (get/put) 27 * @param connectTimeout to tile source 28 * @param readTimeout to tile source 29 * @param headers to be sent with request 30 * @param downloadExecutor that will execute the download task (if needed) 31 */ 32 public WMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile, 33 ICacheAccess<String, BufferedImageCacheEntry> cache, int connectTimeout, int readTimeout, 34 Map<String, String> headers, ThreadPoolExecutor downloadExecutor) { 35 super(listener, tile, cache, connectTimeout, readTimeout, headers, downloadExecutor); 36 } 37 38 /** 39 * If 404 is returned from WMS server, treat this as situation as erroneus 40 */ 41 @Override 42 protected boolean handleNotFound() { 43 return false; 44 } 45 46 /** 47 * Do not try to cache empty responses 48 */ 49 @Override 50 protected boolean cacheAsEmpty(Map<String, List<String>> headers, int statusCode, byte[] content) { 51 if (statusCode < 100 || statusCode > 400) { 52 // cache any errors 53 return true; 54 } 55 return false; 56 } 57 58 } -
deleted file src/org/openstreetmap/josm/data/imagery/WmsCache.java
diff --git a/src/org/openstreetmap/josm/data/imagery/WmsCache.java b/src/org/openstreetmap/josm/data/imagery/WmsCache.java deleted file mode 100644 index 1223fdf..0000000
+ - 1 // License: GPL. For details, see LICENSE file.2 package org.openstreetmap.josm.data.imagery;3 4 import java.awt.Graphics2D;5 import java.awt.image.BufferedImage;6 import java.io.BufferedOutputStream;7 import java.io.File;8 import java.io.FileInputStream;9 import java.io.FileNotFoundException;10 import java.io.FileOutputStream;11 import java.io.IOException;12 import java.io.InputStream;13 import java.io.OutputStream;14 import java.lang.ref.SoftReference;15 import java.net.URLConnection;16 import java.util.ArrayList;17 import java.util.Calendar;18 import java.util.Collections;19 import java.util.Comparator;20 import java.util.HashMap;21 import java.util.HashSet;22 import java.util.Iterator;23 import java.util.List;24 import java.util.Map;25 import java.util.Properties;26 import java.util.Set;27 28 import javax.imageio.ImageIO;29 import javax.xml.bind.JAXBContext;30 import javax.xml.bind.Marshaller;31 import javax.xml.bind.Unmarshaller;32 33 import org.openstreetmap.josm.Main;34 import org.openstreetmap.josm.data.ProjectionBounds;35 import org.openstreetmap.josm.data.SystemOfMeasurement;36 import org.openstreetmap.josm.data.coor.EastNorth;37 import org.openstreetmap.josm.data.coor.LatLon;38 import org.openstreetmap.josm.data.imagery.types.EntryType;39 import org.openstreetmap.josm.data.imagery.types.ProjectionType;40 import org.openstreetmap.josm.data.imagery.types.WmsCacheType;41 import org.openstreetmap.josm.data.preferences.StringProperty;42 import org.openstreetmap.josm.data.projection.Projection;43 import org.openstreetmap.josm.gui.layer.WMSLayer;44 import org.openstreetmap.josm.tools.ImageProvider;45 import org.openstreetmap.josm.tools.Utils;46 import org.openstreetmap.josm.tools.date.DateUtils;47 48 public class WmsCache {49 //TODO Property for maximum cache size50 //TODO Property for maximum age of tile, automatically remove old tiles51 //TODO Measure time for partially loading from cache, compare with time to download tile. If slower, disable partial cache52 //TODO Do loading from partial cache and downloading at the same time, don't wait for partial cache to load53 54 private static final StringProperty PROP_CACHE_PATH = new StringProperty("imagery.wms-cache.path", "wms");55 private static final String INDEX_FILENAME = "index.xml";56 private static final String LAYERS_INDEX_FILENAME = "layers.properties";57 58 private static class CacheEntry {59 private final double pixelPerDegree;60 private final double east;61 private final double north;62 private final ProjectionBounds bounds;63 private final String filename;64 65 private long lastUsed;66 private long lastModified;67 68 CacheEntry(double pixelPerDegree, double east, double north, int tileSize, String filename) {69 this.pixelPerDegree = pixelPerDegree;70 this.east = east;71 this.north = north;72 this.bounds = new ProjectionBounds(east, north, east + tileSize / pixelPerDegree, north + tileSize / pixelPerDegree);73 this.filename = filename;74 }75 76 @Override77 public String toString() {78 return "CacheEntry [pixelPerDegree=" + pixelPerDegree + ", east=" + east + ", north=" + north + ", bounds="79 + bounds + ", filename=" + filename + ", lastUsed=" + lastUsed + ", lastModified=" + lastModified80 + "]";81 }82 }83 84 private static class ProjectionEntries {85 private final String projection;86 private final String cacheDirectory;87 private final List<CacheEntry> entries = new ArrayList<>();88 89 ProjectionEntries(String projection, String cacheDirectory) {90 this.projection = projection;91 this.cacheDirectory = cacheDirectory;92 }93 }94 95 private final Map<String, ProjectionEntries> entries = new HashMap<>();96 private final File cacheDir;97 private final int tileSize; // Should be always 50098 private int totalFileSize;99 private boolean totalFileSizeDirty; // Some file was missing - size needs to be recalculated100 // No need for hashCode/equals on CacheEntry, object identity is enough. Comparing by values can lead to error - CacheEntry for wrong projection could be found101 private Map<CacheEntry, SoftReference<BufferedImage>> memoryCache = new HashMap<>();102 private Set<ProjectionBounds> areaToCache;103 104 protected String cacheDirPath() {105 String cPath = PROP_CACHE_PATH.get();106 if (!(new File(cPath).isAbsolute())) {107 cPath = Main.pref.getCacheDirectory() + File.separator + cPath;108 }109 return cPath;110 }111 112 public WmsCache(String url, int tileSize) {113 File globalCacheDir = new File(cacheDirPath());114 if (!globalCacheDir.mkdirs()) {115 Main.warn("Unable to create global cache directory: "+globalCacheDir.getAbsolutePath());116 }117 cacheDir = new File(globalCacheDir, getCacheDirectory(url));118 cacheDir.mkdirs();119 this.tileSize = tileSize;120 }121 122 private String getCacheDirectory(String url) {123 String cacheDirName = null;124 Properties layersIndex = new Properties();125 File layerIndexFile = new File(cacheDirPath(), LAYERS_INDEX_FILENAME);126 try (InputStream fis = new FileInputStream(layerIndexFile)) {127 layersIndex.load(fis);128 } catch (FileNotFoundException e) {129 Main.error("Unable to load layers index for wms cache (file " + layerIndexFile + " not found)");130 } catch (IOException e) {131 Main.error("Unable to load layers index for wms cache");132 Main.error(e);133 }134 135 for (Object propKey: layersIndex.keySet()) {136 String s = (String)propKey;137 if (url.equals(layersIndex.getProperty(s))) {138 cacheDirName = s;139 break;140 }141 }142 143 if (cacheDirName == null) {144 int counter = 0;145 while (true) {146 counter++;147 if (!layersIndex.keySet().contains(String.valueOf(counter))) {148 break;149 }150 }151 cacheDirName = String.valueOf(counter);152 layersIndex.setProperty(cacheDirName, url);153 try (OutputStream fos = new FileOutputStream(layerIndexFile)) {154 layersIndex.store(fos, "");155 } catch (IOException e) {156 Main.error("Unable to save layer index for wms cache");157 Main.error(e);158 }159 }160 161 return cacheDirName;162 }163 164 private ProjectionEntries getProjectionEntries(Projection projection) {165 return getProjectionEntries(projection.toCode(), projection.getCacheDirectoryName());166 }167 168 private ProjectionEntries getProjectionEntries(String projection, String cacheDirectory) {169 ProjectionEntries result = entries.get(projection);170 if (result == null) {171 result = new ProjectionEntries(projection, cacheDirectory);172 entries.put(projection, result);173 }174 175 return result;176 }177 178 public synchronized void loadIndex() {179 File indexFile = new File(cacheDir, INDEX_FILENAME);180 try {181 JAXBContext context = JAXBContext.newInstance(182 WmsCacheType.class.getPackage().getName(),183 WmsCacheType.class.getClassLoader());184 Unmarshaller unmarshaller = context.createUnmarshaller();185 WmsCacheType cacheEntries;186 try (InputStream is = new FileInputStream(indexFile)) {187 cacheEntries = (WmsCacheType)unmarshaller.unmarshal(is);188 }189 totalFileSize = cacheEntries.getTotalFileSize();190 if (cacheEntries.getTileSize() != tileSize) {191 Main.info("Cache created with different tileSize, cache will be discarded");192 return;193 }194 for (ProjectionType projectionType: cacheEntries.getProjection()) {195 ProjectionEntries projection = getProjectionEntries(projectionType.getName(), projectionType.getCacheDirectory());196 for (EntryType entry: projectionType.getEntry()) {197 CacheEntry ce = new CacheEntry(entry.getPixelPerDegree(), entry.getEast(), entry.getNorth(), tileSize, entry.getFilename());198 ce.lastUsed = entry.getLastUsed().getTimeInMillis();199 ce.lastModified = entry.getLastModified().getTimeInMillis();200 projection.entries.add(ce);201 }202 }203 } catch (Exception e) {204 if (indexFile.exists()) {205 Main.error(e);206 Main.info("Unable to load index for wms-cache, new file will be created");207 } else {208 Main.info("Index for wms-cache doesn't exist, new file will be created");209 }210 }211 212 removeNonReferencedFiles();213 }214 215 private void removeNonReferencedFiles() {216 217 Set<String> usedProjections = new HashSet<>();218 219 for (ProjectionEntries projectionEntries: entries.values()) {220 221 usedProjections.add(projectionEntries.cacheDirectory);222 223 File projectionDir = new File(cacheDir, projectionEntries.cacheDirectory);224 if (projectionDir.exists()) {225 Set<String> referencedFiles = new HashSet<>();226 227 for (CacheEntry ce: projectionEntries.entries) {228 referencedFiles.add(ce.filename);229 }230 231 File[] files = projectionDir.listFiles();232 if (files != null) {233 for (File file: files) {234 if (!referencedFiles.contains(file.getName()) && !file.delete()) {235 Main.warn("Unable to delete file: "+file.getAbsolutePath());236 }237 }238 }239 }240 }241 242 File[] files = cacheDir.listFiles();243 if (files != null) {244 for (File projectionDir: files) {245 if (projectionDir.isDirectory() && !usedProjections.contains(projectionDir.getName())) {246 Utils.deleteDirectory(projectionDir);247 }248 }249 }250 }251 252 private int calculateTotalFileSize() {253 int result = 0;254 for (ProjectionEntries projectionEntries: entries.values()) {255 Iterator<CacheEntry> it = projectionEntries.entries.iterator();256 while (it.hasNext()) {257 CacheEntry entry = it.next();258 File imageFile = getImageFile(projectionEntries, entry);259 if (!imageFile.exists()) {260 it.remove();261 } else {262 result += imageFile.length();263 }264 }265 }266 return result;267 }268 269 public synchronized void saveIndex() {270 WmsCacheType index = new WmsCacheType();271 272 if (totalFileSizeDirty) {273 totalFileSize = calculateTotalFileSize();274 }275 276 index.setTileSize(tileSize);277 index.setTotalFileSize(totalFileSize);278 for (ProjectionEntries projectionEntries: entries.values()) {279 if (!projectionEntries.entries.isEmpty()) {280 ProjectionType projectionType = new ProjectionType();281 projectionType.setName(projectionEntries.projection);282 projectionType.setCacheDirectory(projectionEntries.cacheDirectory);283 index.getProjection().add(projectionType);284 for (CacheEntry ce: projectionEntries.entries) {285 EntryType entry = new EntryType();286 entry.setPixelPerDegree(ce.pixelPerDegree);287 entry.setEast(ce.east);288 entry.setNorth(ce.north);289 Calendar c = Calendar.getInstance();290 c.setTimeInMillis(ce.lastUsed);291 entry.setLastUsed(c);292 c = Calendar.getInstance();293 c.setTimeInMillis(ce.lastModified);294 entry.setLastModified(c);295 entry.setFilename(ce.filename);296 projectionType.getEntry().add(entry);297 }298 }299 }300 try {301 JAXBContext context = JAXBContext.newInstance(302 WmsCacheType.class.getPackage().getName(),303 WmsCacheType.class.getClassLoader());304 Marshaller marshaller = context.createMarshaller();305 try (OutputStream fos = new FileOutputStream(new File(cacheDir, INDEX_FILENAME))) {306 marshaller.marshal(index, fos);307 }308 } catch (Exception e) {309 Main.error("Failed to save wms-cache file");310 Main.error(e);311 }312 }313 314 private File getImageFile(ProjectionEntries projection, CacheEntry entry) {315 return new File(cacheDir, projection.cacheDirectory + "/" + entry.filename);316 }317 318 private BufferedImage loadImage(ProjectionEntries projectionEntries, CacheEntry entry, boolean enforceTransparency) throws IOException {319 synchronized (this) {320 entry.lastUsed = System.currentTimeMillis();321 322 SoftReference<BufferedImage> memCache = memoryCache.get(entry);323 if (memCache != null) {324 BufferedImage result = memCache.get();325 if (result != null) {326 if (enforceTransparency == ImageProvider.isTransparencyForced(result)) {327 return result;328 } else if (Main.isDebugEnabled()) {329 Main.debug("Skipping "+entry+" from memory cache (transparency enforcement)");330 }331 }332 }333 }334 335 try {336 // Reading can't be in synchronized section, it's too slow337 BufferedImage result = ImageProvider.read(getImageFile(projectionEntries, entry), true, enforceTransparency);338 synchronized (this) {339 if (result == null) {340 projectionEntries.entries.remove(entry);341 totalFileSizeDirty = true;342 }343 return result;344 }345 } catch (IOException e) {346 synchronized (this) {347 projectionEntries.entries.remove(entry);348 totalFileSizeDirty = true;349 throw e;350 }351 }352 }353 354 private CacheEntry findEntry(ProjectionEntries projectionEntries, double pixelPerDegree, double east, double north) {355 for (CacheEntry entry: projectionEntries.entries) {356 if (Utils.equalsEpsilon(entry.pixelPerDegree, pixelPerDegree)357 && Utils.equalsEpsilon(entry.east, east) && Utils.equalsEpsilon(entry.north, north))358 return entry;359 }360 return null;361 }362 363 public synchronized boolean hasExactMatch(Projection projection, double pixelPerDegree, double east, double north) {364 ProjectionEntries projectionEntries = getProjectionEntries(projection);365 return findEntry(projectionEntries, pixelPerDegree, east, north) != null;366 }367 368 public BufferedImage getExactMatch(Projection projection, double pixelPerDegree, double east, double north) {369 CacheEntry entry = null;370 ProjectionEntries projectionEntries = null;371 synchronized (this) {372 projectionEntries = getProjectionEntries(projection);373 entry = findEntry(projectionEntries, pixelPerDegree, east, north);374 }375 if (entry != null) {376 try {377 return loadImage(projectionEntries, entry, WMSLayer.PROP_ALPHA_CHANNEL.get());378 } catch (IOException e) {379 Main.error("Unable to load file from wms cache");380 Main.error(e);381 return null;382 }383 }384 return null;385 }386 387 public BufferedImage getPartialMatch(Projection projection, double pixelPerDegree, double east, double north) {388 ProjectionEntries projectionEntries;389 List<CacheEntry> matches;390 synchronized (this) {391 matches = new ArrayList<>();392 393 double minPPD = pixelPerDegree / 5;394 double maxPPD = pixelPerDegree * 5;395 projectionEntries = getProjectionEntries(projection);396 397 double size2 = tileSize / pixelPerDegree;398 double border = tileSize * 0.01; // Make sure not to load neighboring tiles that intersects this tile only slightly399 ProjectionBounds bounds = new ProjectionBounds(east + border, north + border,400 east + size2 - border, north + size2 - border);401 402 //TODO Do not load tile if it is completely overlapped by other tile with better ppd403 for (CacheEntry entry: projectionEntries.entries) {404 if (entry.pixelPerDegree >= minPPD && entry.pixelPerDegree <= maxPPD && entry.bounds.intersects(bounds)) {405 entry.lastUsed = System.currentTimeMillis();406 matches.add(entry);407 }408 }409 410 if (matches.isEmpty())411 return null;412 413 Collections.sort(matches, new Comparator<CacheEntry>() {414 @Override415 public int compare(CacheEntry o1, CacheEntry o2) {416 return Double.compare(o2.pixelPerDegree, o1.pixelPerDegree);417 }418 });419 }420 421 // Use alpha layer only when enabled on wms layer422 boolean alpha = WMSLayer.PROP_ALPHA_CHANNEL.get();423 BufferedImage result = new BufferedImage(tileSize, tileSize,424 alpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR);425 Graphics2D g = result.createGraphics();426 427 boolean drawAtLeastOnce = false;428 Map<CacheEntry, SoftReference<BufferedImage>> localCache = new HashMap<>();429 for (CacheEntry ce: matches) {430 BufferedImage img;431 try {432 // Enforce transparency only when alpha enabled on wms layer too433 img = loadImage(projectionEntries, ce, alpha);434 localCache.put(ce, new SoftReference<>(img));435 } catch (IOException e) {436 continue;437 }438 439 drawAtLeastOnce = true;440 441 int xDiff = (int)((ce.east - east) * pixelPerDegree);442 int yDiff = (int)((ce.north - north) * pixelPerDegree);443 int size = (int)(pixelPerDegree / ce.pixelPerDegree * tileSize);444 445 int x = xDiff;446 int y = -size + tileSize - yDiff;447 448 g.drawImage(img, x, y, size, size, null);449 }450 451 if (drawAtLeastOnce) {452 synchronized (this) {453 memoryCache.putAll(localCache);454 }455 return result;456 } else457 return null;458 }459 460 private String generateFileName(ProjectionEntries projectionEntries, double pixelPerDegree, Projection projection, double east, double north, String mimeType) {461 LatLon ll1 = projection.eastNorth2latlon(new EastNorth(east, north));462 LatLon ll2 = projection.eastNorth2latlon(new EastNorth(east + 100 / pixelPerDegree, north));463 LatLon ll3 = projection.eastNorth2latlon(new EastNorth(east + tileSize / pixelPerDegree, north + tileSize / pixelPerDegree));464 465 double deltaLat = Math.abs(ll3.lat() - ll1.lat());466 double deltaLon = Math.abs(ll3.lon() - ll1.lon());467 int precisionLat = Math.max(0, -(int)Math.ceil(Math.log10(deltaLat)) + 1);468 int precisionLon = Math.max(0, -(int)Math.ceil(Math.log10(deltaLon)) + 1);469 470 String zoom = SystemOfMeasurement.METRIC.getDistText(ll1.greatCircleDistance(ll2));471 String extension = "dat";472 if (mimeType != null) {473 switch(mimeType) {474 case "image/jpeg":475 case "image/jpg":476 extension = "jpg";477 break;478 case "image/png":479 extension = "png";480 break;481 case "image/gif":482 extension = "gif";483 break;484 default:485 Main.warn("Unrecognized MIME type: "+mimeType);486 }487 }488 489 int counter = 0;490 FILENAME_LOOP:491 while (true) {492 String result = String.format("%s_%." + precisionLat + "f_%." + precisionLon +"f%s.%s", zoom, ll1.lat(), ll1.lon(), counter==0?"":"_" + counter, extension);493 for (CacheEntry entry: projectionEntries.entries) {494 if (entry.filename.equals(result)) {495 counter++;496 continue FILENAME_LOOP;497 }498 }499 return result;500 }501 }502 503 /**504 *505 * @param img Used only when overlapping is used, when not used, used raw from imageData506 * @param imageData507 * @param projection508 * @param pixelPerDegree509 * @param east510 * @param north511 * @throws IOException512 */513 public synchronized void saveToCache(BufferedImage img, InputStream imageData, Projection projection, double pixelPerDegree, double east, double north) throws IOException {514 ProjectionEntries projectionEntries = getProjectionEntries(projection);515 CacheEntry entry = findEntry(projectionEntries, pixelPerDegree, east, north);516 File imageFile;517 if (entry == null) {518 519 String mimeType;520 if (img != null) {521 mimeType = "image/png";522 } else {523 mimeType = URLConnection.guessContentTypeFromStream(imageData);524 }525 entry = new CacheEntry(pixelPerDegree, east, north, tileSize,generateFileName(projectionEntries, pixelPerDegree, projection, east, north, mimeType));526 entry.lastUsed = System.currentTimeMillis();527 entry.lastModified = entry.lastUsed;528 projectionEntries.entries.add(entry);529 imageFile = getImageFile(projectionEntries, entry);530 } else {531 imageFile = getImageFile(projectionEntries, entry);532 totalFileSize -= imageFile.length();533 }534 535 if (!imageFile.getParentFile().mkdirs()) {536 Main.warn("Unable to create parent directory: "+imageFile.getParentFile().getAbsolutePath());537 }538 539 if (img != null) {540 BufferedImage copy = new BufferedImage(tileSize, tileSize, img.getType());541 copy.createGraphics().drawImage(img, 0, 0, tileSize, tileSize, 0, img.getHeight() - tileSize, tileSize, img.getHeight(), null);542 ImageIO.write(copy, "png", imageFile);543 totalFileSize += imageFile.length();544 } else {545 try (OutputStream os = new BufferedOutputStream(new FileOutputStream(imageFile))) {546 totalFileSize += Utils.copyStream(imageData, os);547 }548 }549 }550 551 public synchronized void cleanSmallFiles(int size) {552 for (ProjectionEntries projectionEntries: entries.values()) {553 Iterator<CacheEntry> it = projectionEntries.entries.iterator();554 while (it.hasNext()) {555 File file = getImageFile(projectionEntries, it.next());556 long length = file.length();557 if (length <= size) {558 if (length == 0) {559 totalFileSizeDirty = true; // File probably doesn't exist560 }561 totalFileSize -= size;562 if (!file.delete()) {563 Main.warn("Unable to delete file: "+file.getAbsolutePath());564 }565 it.remove();566 }567 }568 }569 }570 571 public static String printDate(Calendar c) {572 return DateUtils.newIsoDateFormat().format(c.getTime());573 }574 575 private boolean isInsideAreaToCache(CacheEntry cacheEntry) {576 for (ProjectionBounds b: areaToCache) {577 if (cacheEntry.bounds.intersects(b))578 return true;579 }580 return false;581 }582 583 public synchronized void setAreaToCache(Set<ProjectionBounds> areaToCache) {584 this.areaToCache = areaToCache;585 Iterator<CacheEntry> it = memoryCache.keySet().iterator();586 while (it.hasNext()) {587 if (!isInsideAreaToCache(it.next())) {588 it.remove();589 }590 }591 }592 } -
src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
diff --git a/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java b/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java index 3effadd..09b976c 100644
a b import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 29 29 import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; 30 30 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 31 31 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 32 import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; 32 33 import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource; 33 34 import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource; 34 35 import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource; … … public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser { 69 70 continue; 70 71 } 71 72 try { 72 TileSource source = TMSLayer.getTileSource(info);73 TileSource source = AbstractTMSTileSource.getTileSource(info); 73 74 if (source != null) { 74 75 sources.add(source); 75 76 } -
new file src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java new file mode 100644 index 0000000..c8b7fc1
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.Color; 7 import java.awt.Component; 8 import java.awt.Font; 9 import java.awt.Graphics; 10 import java.awt.Graphics2D; 11 import java.awt.GridBagLayout; 12 import java.awt.Image; 13 import java.awt.Point; 14 import java.awt.Rectangle; 15 import java.awt.Toolkit; 16 import java.awt.event.ActionEvent; 17 import java.awt.event.MouseAdapter; 18 import java.awt.event.MouseEvent; 19 import java.awt.image.ImageObserver; 20 import java.io.File; 21 import java.io.IOException; 22 import java.text.SimpleDateFormat; 23 import java.util.ArrayList; 24 import java.util.Collections; 25 import java.util.Comparator; 26 import java.util.Date; 27 import java.util.LinkedList; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Map.Entry; 31 import java.util.Set; 32 import java.util.concurrent.ConcurrentSkipListSet; 33 34 import javax.swing.AbstractAction; 35 import javax.swing.Action; 36 import javax.swing.BorderFactory; 37 import javax.swing.JCheckBoxMenuItem; 38 import javax.swing.JLabel; 39 import javax.swing.JMenuItem; 40 import javax.swing.JOptionPane; 41 import javax.swing.JPanel; 42 import javax.swing.JPopupMenu; 43 import javax.swing.JTextField; 44 45 import org.openstreetmap.gui.jmapviewer.AttributionSupport; 46 import org.openstreetmap.gui.jmapviewer.Coordinate; 47 import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 48 import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 49 import org.openstreetmap.gui.jmapviewer.Tile; 50 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; 51 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 52 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 53 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 54 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 55 import org.openstreetmap.josm.Main; 56 import org.openstreetmap.josm.actions.RenameLayerAction; 57 import org.openstreetmap.josm.actions.SaveActionBase; 58 import org.openstreetmap.josm.data.Bounds; 59 import org.openstreetmap.josm.data.coor.EastNorth; 60 import org.openstreetmap.josm.data.coor.LatLon; 61 import org.openstreetmap.josm.data.imagery.ImageryInfo; 62 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 63 import org.openstreetmap.josm.data.imagery.TileLoaderFactory; 64 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 65 import org.openstreetmap.josm.data.preferences.BooleanProperty; 66 import org.openstreetmap.josm.data.preferences.IntegerProperty; 67 import org.openstreetmap.josm.gui.ExtendedDialog; 68 import org.openstreetmap.josm.gui.MapFrame; 69 import org.openstreetmap.josm.gui.MapView; 70 import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 71 import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; 72 import org.openstreetmap.josm.gui.PleaseWaitRunnable; 73 import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 74 import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 75 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 76 import org.openstreetmap.josm.io.WMSLayerImporter; 77 import org.openstreetmap.josm.tools.GBC; 78 79 /** 80 * 81 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS 82 * 83 * It implements all standard functions of tilesource based layers: autozoom, tile reloads, layer saving, loading,etc. 84 * 85 * @author Wiktor Niesiobędzki 86 * @since TODO 87 * 88 */ 89 public abstract class AbstractTileSourceLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener { 90 private static final String PREFERENCE_PREFIX = "imagery.generic"; 91 92 /** maximum zoom level supported */ 93 public static final int MAX_ZOOM = 30; 94 /** minium zoom level supported */ 95 public static final int MIN_ZOOM = 2; 96 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13); 97 98 /** do set autozoom when creating a new layer */ 99 public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true); 100 /** do set autoload when creating a new layer */ 101 public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true); 102 /** do set showerrors when creating a new layer */ 103 public static final BooleanProperty PROP_DEFAULT_SHOWERRORS = new BooleanProperty(PREFERENCE_PREFIX + ".default_showerrors", true); 104 /** minimum zoom level to show to user */ 105 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2); 106 /** maximum zoom level to show to user */ 107 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20); 108 109 //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false); 110 /** 111 * Zoomlevel at which tiles is currently downloaded. 112 * Initial zoom lvl is set to bestZoom 113 */ 114 public int currentZoomLevel; 115 private boolean needRedraw; 116 117 private AttributionSupport attribution = new AttributionSupport(); 118 Tile showMetadataTile; 119 120 // needed public access for session exporter 121 /** if layers changes automatically, when user zooms in */ 122 public boolean autoZoom; 123 /** if layer automatically loads new tiles */ 124 public boolean autoLoad; 125 /** if layer should show errors on tiles */ 126 public boolean showErrors; 127 128 protected TileCache tileCache; 129 protected TileSource tileSource; 130 protected TileLoader tileLoader; 131 132 /** 133 * Creates Tile Source based Imagery Layer based on Imagery Info 134 * @param info 135 */ 136 public AbstractTileSourceLayer(ImageryInfo info) { 137 super(info); 138 139 if(!isProjectionSupported(Main.getProjection())) { 140 JOptionPane.showMessageDialog(Main.parent, 141 tr("This layer do not support the projection {0}.\n{1}\n" 142 + "Change the projection or remove the layer.", 143 Main.getProjection().toCode(), nameSupportedProjections()), 144 tr("Warning"), 145 JOptionPane.WARNING_MESSAGE); 146 } 147 setBackgroundLayer(true); 148 this.setVisible(true); 149 150 initTileSource(getTileSource(info)); 151 MapView.addZoomChangeListener(this); 152 } 153 154 protected abstract TileLoaderFactory getTileLoaderFactory(); 155 156 /** 157 * 158 * @param info 159 * @return TileSource for specified ImageryInfo 160 * @throws IllegalArgumentException when Imagery is not supported by layer 161 */ 162 protected abstract TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException; 163 164 protected abstract Map<String, String> getHeaders(TileSource tileSource); 165 166 protected void initTileSource(TileSource tileSource) { 167 this.tileSource = tileSource; 168 attribution.initialize(tileSource); 169 170 currentZoomLevel = getBestZoom(); 171 172 Map<String, String> headers = getHeaders(tileSource); 173 174 tileLoader = getTileLoaderFactory().makeTileLoader(this, headers); 175 if (tileLoader instanceof TMSCachedTileLoader) { 176 tileCache = (TileCache) tileLoader; 177 } else { 178 tileCache = new MemoryTileCache(); 179 } 180 if (tileLoader == null) 181 tileLoader = new OsmTileLoader(this); 182 } 183 184 185 186 @Override 187 public synchronized void tileLoadingFinished(Tile tile, boolean success) { 188 if (tile.hasError()) { 189 success = false; 190 tile.setImage(null); 191 } 192 if (sharpenLevel != 0 && success) { 193 tile.setImage(sharpenImage(tile.getImage())); 194 } 195 tile.setLoaded(success); 196 needRedraw = true; 197 if (Main.map != null) { 198 Main.map.repaint(100); 199 } 200 if (Main.isDebugEnabled()) { 201 Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success); 202 } 203 } 204 205 /** 206 * Clears the tile cache. 207 * 208 * If the current tileLoader is an instance of OsmTileLoader, a new 209 * TmsTileClearController is created and passed to the according clearCache 210 * method. 211 * 212 * @param monitor not used in this implementation - as cache clear is instaneus 213 */ 214 public void clearTileCache(ProgressMonitor monitor) { 215 tileCache.clear(); 216 if (tileLoader instanceof CachedTileLoader) { 217 ((CachedTileLoader)tileLoader).clearCache(tileSource); 218 } 219 } 220 221 222 /** 223 * Initiates a repaint of Main.map 224 * 225 * @see Main#map 226 * @see MapFrame#repaint() 227 */ 228 protected void redraw() { 229 needRedraw = true; 230 Main.map.repaint(); 231 } 232 233 /** 234 * Marks layer as needing redraw on offset change 235 */ 236 @Override 237 public void setOffset(double dx, double dy) { 238 super.setOffset(dx, dy); 239 needRedraw = true; 240 } 241 242 243 /** 244 * Returns average number of screen pixels per tile pixel for current mapview 245 */ 246 private double getScaleFactor(int zoom) { 247 if (!Main.isDisplayingMapView()) return 1; 248 MapView mv = Main.map.mapView; 249 LatLon topLeft = mv.getLatLon(0, 0); 250 LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight()); 251 double x1 = tileSource.lonToTileX(topLeft.lon(), zoom); 252 double y1 = tileSource.latToTileY(topLeft.lat(), zoom); 253 double x2 = tileSource.lonToTileX(botRight.lon(), zoom); 254 double y2 = tileSource.latToTileY(botRight.lat(), zoom); 255 256 int screenPixels = mv.getWidth()*mv.getHeight(); 257 double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize()); 258 if (screenPixels == 0 || tilePixels == 0) return 1; 259 return screenPixels/tilePixels; 260 } 261 262 private final int getBestZoom() { 263 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view 264 double result = Math.log(factor)/Math.log(2)/2+1; 265 /* 266 * Math.log(factor)/Math.log(2) - gives log base 2 of factor 267 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2 268 * In general, smaller zoom levels are more readable. We prefer big, 269 * block, pixelated (but readable) map text to small, smeared, 270 * unreadable underzoomed text. So, use .floor() instead of rounding 271 * to skew things a bit toward the lower zooms. 272 * Remember, that result here, should correspond to TMSLayer.paint(...) 273 * getScaleFactor(...) is supposed to be between 0.75 and 3 274 */ 275 int intResult = (int)Math.floor(result); 276 if (intResult > getMaxZoomLvl()) 277 return getMaxZoomLvl(); 278 if (intResult < getMinZoomLvl()) 279 return getMinZoomLvl(); 280 return intResult; 281 } 282 283 284 private final static boolean actionSupportLayers(List<Layer> layers) { 285 return layers.size() == 1 && layers.get(0) instanceof TMSLayer; 286 } 287 288 private class AutoZoomAction extends AbstractAction implements LayerAction { 289 public AutoZoomAction() { 290 super(tr("Auto Zoom")); 291 } 292 @Override 293 public void actionPerformed(ActionEvent ae) { 294 autoZoom = !autoZoom; 295 } 296 297 public Component createMenuComponent() { 298 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 299 item.setSelected(autoZoom); 300 return item; 301 } 302 @Override 303 public boolean supportLayers(List<Layer> layers) { 304 return actionSupportLayers(layers); 305 } 306 307 } 308 309 private class AutoLoadTilesAction extends AbstractAction implements LayerAction { 310 public AutoLoadTilesAction() { 311 super(tr("Auto load tiles")); 312 } 313 @Override 314 public void actionPerformed(ActionEvent ae) { 315 autoLoad= !autoLoad; 316 } 317 318 public Component createMenuComponent() { 319 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 320 item.setSelected(autoLoad); 321 return item; 322 } 323 @Override 324 public boolean supportLayers(List<Layer> layers) { 325 return actionSupportLayers(layers); 326 } 327 } 328 329 private class LoadAllTilesAction extends AbstractAction { 330 public LoadAllTilesAction() { 331 super(tr("Load All Tiles")); 332 } 333 @Override 334 public void actionPerformed(ActionEvent ae) { 335 loadAllTiles(true); 336 redraw(); 337 } 338 } 339 340 private class LoadErroneusTilesAction extends AbstractAction { 341 public LoadErroneusTilesAction() { 342 super(tr("Load All Error Tiles")); 343 } 344 345 @Override 346 public void actionPerformed(ActionEvent ae) { 347 loadAllErrorTiles(true); 348 redraw(); 349 } 350 } 351 352 private class ZoomToNativeLevelAction extends AbstractAction { 353 public ZoomToNativeLevelAction() { 354 super(tr("Zoom to native resolution")); 355 } 356 @Override 357 public void actionPerformed(ActionEvent ae) { 358 double new_factor = Math.sqrt(getScaleFactor(currentZoomLevel)); 359 Main.map.mapView.zoomToFactor(new_factor); 360 redraw(); 361 } 362 } 363 364 private class ZoomToBestAction extends AbstractAction { 365 public ZoomToBestAction() { 366 super(tr("Change resolution")); 367 } 368 @Override 369 public void actionPerformed(ActionEvent ae) { 370 setZoomLevel(getBestZoom()); 371 } 372 } 373 374 /* 375 * Simple class to keep clickedTile within hookUpMapView 376 */ 377 private class TileHolder { 378 private Tile t = null; 379 380 public Tile getTile() { 381 return t; 382 } 383 384 public void setTile(Tile t) { 385 this.t = t; 386 } 387 } 388 389 /** 390 * Creates popup menu items and binds to mouse actions 391 */ 392 @Override 393 public void hookUpMapView() { 394 // keep them final here, so we avoid namespace clutter in the class 395 final JPopupMenu tileOptionMenu = new JPopupMenu(); 396 final TileHolder clickedTileHolder = new TileHolder(); 397 398 autoZoom = PROP_DEFAULT_AUTOZOOM.get(); 399 JCheckBoxMenuItem autoZoomPopup = new JCheckBoxMenuItem(); 400 autoZoomPopup.setAction(new AutoZoomAction()); 401 autoZoomPopup.setSelected(autoZoom); 402 tileOptionMenu.add(autoZoomPopup); 403 404 autoLoad = PROP_DEFAULT_AUTOLOAD.get(); 405 JCheckBoxMenuItem autoLoadPopup = new JCheckBoxMenuItem(); 406 autoLoadPopup.setAction(new AutoLoadTilesAction()); 407 autoLoadPopup.setSelected(autoLoad); 408 tileOptionMenu.add(autoLoadPopup); 409 410 showErrors = PROP_DEFAULT_SHOWERRORS.get(); 411 JCheckBoxMenuItem showErrorsPopup = new JCheckBoxMenuItem(); 412 showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) { 413 @Override 414 public void actionPerformed(ActionEvent ae) { 415 showErrors = !showErrors; 416 } 417 }); 418 showErrorsPopup.setSelected(showErrors); 419 tileOptionMenu.add(showErrorsPopup); 420 421 tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) { 422 @Override 423 public void actionPerformed(ActionEvent ae) { 424 Tile clickedTile = clickedTileHolder.getTile(); 425 if (clickedTile != null) { 426 loadTile(clickedTile, true); 427 redraw(); 428 } 429 } 430 })); 431 432 tileOptionMenu.add(new JMenuItem(new AbstractAction( 433 tr("Show Tile Info")) { 434 private String getSizeString(int size) { 435 StringBuilder ret = new StringBuilder(); 436 return ret.append(size).append("x").append(size).toString(); 437 } 438 439 private JTextField createTextField(String text) { 440 JTextField ret = new JTextField(text); 441 ret.setEditable(false); 442 ret.setBorder(BorderFactory.createEmptyBorder()); 443 return ret; 444 } 445 446 @Override 447 public void actionPerformed(ActionEvent ae) { 448 Tile clickedTile = clickedTileHolder.getTile(); 449 if (clickedTile != null) { 450 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")}); 451 JPanel panel = new JPanel(new GridBagLayout()); 452 Rectangle displaySize = tileToRect(clickedTile); 453 String url = ""; 454 try { 455 url = clickedTile.getUrl(); 456 } catch (IOException e) { 457 // silence exceptions 458 } 459 460 String[][] content = { 461 {"Tile name", clickedTile.getKey()}, 462 {"Tile url", url}, 463 {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) }, 464 {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()}, 465 }; 466 467 for (String[] entry: content) { 468 panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std()); 469 panel.add(GBC.glue(5,0), GBC.std()); 470 panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL)); 471 } 472 473 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) { 474 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std()); 475 panel.add(GBC.glue(5,0), GBC.std()); 476 String value = e.getValue(); 477 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) { 478 value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value))); 479 } 480 panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL)); 481 482 } 483 ed.setIcon(JOptionPane.INFORMATION_MESSAGE); 484 ed.setContent(panel); 485 ed.showDialog(); 486 } 487 } 488 })); 489 490 tileOptionMenu.add(new JMenuItem(new LoadAllTilesAction())); 491 tileOptionMenu.add(new JMenuItem(new LoadErroneusTilesAction())); 492 493 // increase and decrease commands 494 tileOptionMenu.add(new JMenuItem(new AbstractAction( 495 tr("Increase zoom")) { 496 @Override 497 public void actionPerformed(ActionEvent ae) { 498 increaseZoomLevel(); 499 redraw(); 500 } 501 })); 502 503 tileOptionMenu.add(new JMenuItem(new AbstractAction( 504 tr("Decrease zoom")) { 505 @Override 506 public void actionPerformed(ActionEvent ae) { 507 decreaseZoomLevel(); 508 redraw(); 509 } 510 })); 511 512 tileOptionMenu.add(new JMenuItem(new AbstractAction( 513 tr("Snap to tile size")) { 514 @Override 515 public void actionPerformed(ActionEvent ae) { 516 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel)); 517 Main.map.mapView.zoomToFactor(newFactor); 518 redraw(); 519 } 520 })); 521 522 tileOptionMenu.add(new JMenuItem(new AbstractAction( 523 tr("Flush Tile Cache")) { 524 @Override 525 public void actionPerformed(ActionEvent ae) { 526 new PleaseWaitRunnable(tr("Flush Tile Cache")) { 527 @Override 528 protected void realRun() { 529 clearTileCache(getProgressMonitor()); 530 } 531 532 @Override 533 protected void finish() { 534 } 535 536 @Override 537 protected void cancel() { 538 } 539 }.run(); 540 } 541 })); 542 543 final MouseAdapter adapter = new MouseAdapter() { 544 @Override 545 public void mouseClicked(MouseEvent e) { 546 if (!isVisible()) return; 547 if (e.getButton() == MouseEvent.BUTTON3) { 548 clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY())); 549 tileOptionMenu.show(e.getComponent(), e.getX(), e.getY()); 550 } else if (e.getButton() == MouseEvent.BUTTON1) { 551 attribution.handleAttribution(e.getPoint(), true); 552 } 553 } 554 }; 555 Main.map.mapView.addMouseListener(adapter); 556 557 MapView.addLayerChangeListener(new LayerChangeListener() { 558 @Override 559 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 560 // 561 } 562 563 @Override 564 public void layerAdded(Layer newLayer) { 565 // 566 } 567 568 @Override 569 public void layerRemoved(Layer oldLayer) { 570 if (oldLayer == AbstractTileSourceLayer.this) { 571 Main.map.mapView.removeMouseListener(adapter); 572 MapView.removeLayerChangeListener(this); 573 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this); 574 } 575 } 576 }); 577 } 578 579 /** 580 * Checks zoom level against settings 581 * @param maxZoomLvl zoom level to check 582 * @param ts tile source to crosscheck with 583 * @return maximum zoom level, not higher than supported by tilesource nor set by the user 584 */ 585 public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) { 586 if(maxZoomLvl > MAX_ZOOM) { 587 maxZoomLvl = MAX_ZOOM; 588 } 589 if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) { 590 maxZoomLvl = PROP_MIN_ZOOM_LVL.get(); 591 } 592 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) { 593 maxZoomLvl = ts.getMaxZoom(); 594 } 595 return maxZoomLvl; 596 } 597 598 /** 599 * Checks zoom level against settings 600 * @param minZoomLvl zoom level to check 601 * @param ts tile source to crosscheck with 602 * @return minimum zoom level, not higher than supported by tilesource nor set by the user 603 */ 604 public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) { 605 if(minZoomLvl < MIN_ZOOM) { 606 minZoomLvl = MIN_ZOOM; 607 } 608 if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) { 609 minZoomLvl = getMaxZoomLvl(ts); 610 } 611 if (ts != null && ts.getMinZoom() > minZoomLvl) { 612 minZoomLvl = ts.getMinZoom(); 613 } 614 return minZoomLvl; 615 } 616 617 618 /** 619 * @param ts TileSource for which we want to know maximum zoom level 620 * @return maximum max zoom level, that will be shown on layer 621 */ 622 public static int getMaxZoomLvl(TileSource ts) { 623 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts); 624 } 625 626 /** 627 * @param ts TileSource for which we want to know minimum zoom level 628 * @return minimum zoom level, that will be shown on layer 629 */ 630 public static int getMinZoomLvl(TileSource ts) { 631 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts); 632 } 633 634 635 /** 636 * Sets maximum zoom level, that layer will attempt show 637 * @param maxZoomLvl 638 */ 639 public static void setMaxZoomLvl(int maxZoomLvl) { 640 maxZoomLvl = checkMaxZoomLvl(maxZoomLvl, null); 641 PROP_MAX_ZOOM_LVL.put(maxZoomLvl); 642 } 643 644 /** 645 * Sets minimum zoom level, that layer will attempt show 646 * @param minZoomLvl 647 */ 648 public static void setMinZoomLvl(int minZoomLvl) { 649 minZoomLvl = checkMinZoomLvl(minZoomLvl, null); 650 PROP_MIN_ZOOM_LVL.put(minZoomLvl); 651 } 652 653 654 /** 655 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all 656 * changes to visible map (panning/zooming) 657 */ 658 @Override 659 public void zoomChanged() { 660 if (Main.isDebugEnabled()) { 661 Main.debug("zoomChanged(): " + currentZoomLevel); 662 } 663 if (tileLoader instanceof TMSCachedTileLoader) { 664 ((TMSCachedTileLoader)tileLoader).cancelOutstandingTasks(); 665 } 666 needRedraw = true; 667 } 668 669 protected int getMaxZoomLvl() { 670 if (info.getMaxZoom() != 0) 671 return checkMaxZoomLvl(info.getMaxZoom(), tileSource); 672 else 673 return getMaxZoomLvl(tileSource); 674 } 675 676 protected int getMinZoomLvl() { 677 return getMinZoomLvl(tileSource); 678 } 679 680 /** 681 * 682 * @return if its allowed to zoom in 683 */ 684 public boolean zoomIncreaseAllowed() { 685 boolean zia = currentZoomLevel < this.getMaxZoomLvl(); 686 if (Main.isDebugEnabled()) { 687 Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() ); 688 } 689 return zia; 690 } 691 692 /** 693 * Zoom in, go closer to map. 694 * 695 * @return true, if zoom increasing was successful, false otherwise 696 */ 697 public boolean increaseZoomLevel() { 698 if (zoomIncreaseAllowed()) { 699 currentZoomLevel++; 700 if (Main.isDebugEnabled()) { 701 Main.debug("increasing zoom level to: " + currentZoomLevel); 702 } 703 zoomChanged(); 704 } else { 705 Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+ 706 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached."); 707 return false; 708 } 709 return true; 710 } 711 712 /** 713 * Sets the zoom level of the layer 714 * @param zoom zoom level 715 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels 716 */ 717 public boolean setZoomLevel(int zoom) { 718 if (zoom == currentZoomLevel) return true; 719 if (zoom > this.getMaxZoomLvl()) return false; 720 if (zoom < this.getMinZoomLvl()) return false; 721 currentZoomLevel = zoom; 722 zoomChanged(); 723 return true; 724 } 725 726 /** 727 * Check if zooming out is allowed 728 * 729 * @return true, if zooming out is allowed (currentZoomLevel > minZoomLevel) 730 */ 731 public boolean zoomDecreaseAllowed() { 732 return currentZoomLevel > this.getMinZoomLvl(); 733 } 734 735 /** 736 * Zoom out from map. 737 * 738 * @return true, if zoom increasing was successfull, false othervise 739 */ 740 public boolean decreaseZoomLevel() { 741 //int minZoom = this.getMinZoomLvl(); 742 if (zoomDecreaseAllowed()) { 743 if (Main.isDebugEnabled()) { 744 Main.debug("decreasing zoom level to: " + currentZoomLevel); 745 } 746 currentZoomLevel--; 747 zoomChanged(); 748 } else { 749 /*Main.debug("Current zoom level could not be decreased. Min. zoom level "+minZoom+" reached.");*/ 750 return false; 751 } 752 return true; 753 } 754 755 /* 756 * We use these for quick, hackish calculations. They 757 * are temporary only and intentionally not inserted 758 * into the tileCache. 759 */ 760 private Tile tempCornerTile(Tile t) { 761 int x = t.getXtile() + 1; 762 int y = t.getYtile() + 1; 763 int zoom = t.getZoom(); 764 Tile tile = getTile(x, y, zoom); 765 if (tile != null) 766 return tile; 767 return new Tile(tileSource, x, y, zoom); 768 } 769 770 private Tile getOrCreateTile(int x, int y, int zoom) { 771 Tile tile = getTile(x, y, zoom); 772 if (tile == null) { 773 tile = new Tile(tileSource, x, y, zoom); 774 tileCache.addTile(tile); 775 tile.loadPlaceholderFromCache(tileCache); 776 } 777 return tile; 778 } 779 780 /* 781 * This can and will return null for tiles that are not 782 * already in the cache. 783 */ 784 private Tile getTile(int x, int y, int zoom) { 785 int max = (1 << zoom); 786 if (x < 0 || x >= max || y < 0 || y >= max) 787 return null; 788 return tileCache.getTile(tileSource, x, y, zoom); 789 } 790 791 private boolean loadTile(Tile tile, boolean force) { 792 if (tile == null) 793 return false; 794 if (!force && (tile.isLoaded() || tile.hasError())) 795 return false; 796 if (tile.isLoading()) 797 return false; 798 tileLoader.createTileLoaderJob(tile).submit(); 799 return true; 800 } 801 802 private TileSet getVisibleTileSet() { 803 MapView mv = Main.map.mapView; 804 EastNorth topLeft = mv.getEastNorth(0, 0); 805 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight()); 806 return new TileSet(topLeft, botRight, currentZoomLevel); 807 } 808 809 private void loadAllTiles(boolean force) { 810 TileSet ts = getVisibleTileSet(); 811 812 // if there is more than 18 tiles on screen in any direction, do not 813 // load all tiles! 814 if (ts.tooLarge()) { 815 Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!"); 816 return; 817 } 818 ts.loadAllTiles(force); 819 } 820 821 private void loadAllErrorTiles(boolean force) { 822 TileSet ts = getVisibleTileSet(); 823 ts.loadAllErrorTiles(force); 824 } 825 826 @Override 827 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { 828 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0; 829 needRedraw = true; 830 if (Main.isDebugEnabled()) { 831 Main.debug("imageUpdate() done: " + done + " calling repaint"); 832 } 833 Main.map.repaint(done ? 0 : 100); 834 return !done; 835 } 836 837 private boolean imageLoaded(Image i) { 838 if (i == null) 839 return false; 840 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this); 841 if ((status & ALLBITS) != 0) 842 return true; 843 return false; 844 } 845 846 /** 847 * Returns the image for the given tile if both tile and image are loaded. 848 * Otherwise returns null. 849 * 850 * @param tile the Tile for which the image should be returned 851 * @return the image of the tile or null. 852 */ 853 private Image getLoadedTileImage(Tile tile) { 854 if (!tile.isLoaded()) 855 return null; 856 Image img = tile.getImage(); 857 if (!imageLoaded(img)) 858 return null; 859 return img; 860 } 861 862 private LatLon tileLatLon(Tile t) { 863 int zoom = t.getZoom(); 864 return new LatLon(tileSource.tileYToLat(t.getYtile(), zoom), 865 tileSource.tileXToLon(t.getXtile(), zoom)); 866 } 867 868 private Rectangle tileToRect(Tile t1) { 869 /* 870 * We need to get a box in which to draw, so advance by one tile in 871 * each direction to find the other corner of the box. 872 * Note: this somewhat pollutes the tile cache 873 */ 874 Tile t2 = tempCornerTile(t1); 875 Rectangle rect = new Rectangle(pixelPos(t1)); 876 rect.add(pixelPos(t2)); 877 return rect; 878 } 879 880 // 'source' is the pixel coordinates for the area that 881 // the img is capable of filling in. However, we probably 882 // only want a portion of it. 883 // 884 // 'border' is the screen cordinates that need to be drawn. 885 // We must not draw outside of it. 886 private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) { 887 Rectangle target = source; 888 889 // If a border is specified, only draw the intersection 890 // if what we have combined with what we are supposed 891 // to draw. 892 if (border != null) { 893 target = source.intersection(border); 894 if (Main.isDebugEnabled()) { 895 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target); 896 } 897 } 898 899 // All of the rectangles are in screen coordinates. We need 900 // to how these correlate to the sourceImg pixels. We could 901 // avoid doing this by scaling the image up to the 'source' size, 902 // but this should be cheaper. 903 // 904 // In some projections, x any y are scaled differently enough to 905 // cause a pixel or two of fudge. Calculate them separately. 906 double imageYScaling = sourceImg.getHeight(this) / source.getHeight(); 907 double imageXScaling = sourceImg.getWidth(this) / source.getWidth(); 908 909 // How many pixels into the 'source' rectangle are we drawing? 910 int screen_x_offset = target.x - source.x; 911 int screen_y_offset = target.y - source.y; 912 // And how many pixels into the image itself does that 913 // correlate to? 914 int img_x_offset = (int)(screen_x_offset * imageXScaling + 0.5); 915 int img_y_offset = (int)(screen_y_offset * imageYScaling + 0.5); 916 // Now calculate the other corner of the image that we need 917 // by scaling the 'target' rectangle's dimensions. 918 int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling + 0.5); 919 int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling + 0.5); 920 921 if (Main.isDebugEnabled()) { 922 Main.debug("drawing image into target rect: " + target); 923 } 924 g.drawImage(sourceImg, 925 target.x, target.y, 926 target.x + target.width, target.y + target.height, 927 img_x_offset, img_y_offset, 928 img_x_end, img_y_end, 929 this); 930 if (PROP_FADE_AMOUNT.get() != 0) { 931 // dimm by painting opaque rect... 932 g.setColor(getFadeColorWithAlpha()); 933 g.fillRect(target.x, target.y, 934 target.width, target.height); 935 } 936 } 937 938 // This function is called for several zoom levels, not just 939 // the current one. It should not trigger any tiles to be 940 // downloaded. It should also avoid polluting the tile cache 941 // with any tiles since these tiles are not mandatory. 942 // 943 // The "border" tile tells us the boundaries of where we may 944 // draw. It will not be from the zoom level that is being 945 // drawn currently. If drawing the displayZoomLevel, 946 // border is null and we draw the entire tile set. 947 private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) { 948 if (zoom <= 0) return Collections.emptyList(); 949 Rectangle borderRect = null; 950 if (border != null) { 951 borderRect = tileToRect(border); 952 } 953 List<Tile> missedTiles = new LinkedList<>(); 954 // The callers of this code *require* that we return any tiles 955 // that we do not draw in missedTiles. ts.allExistingTiles() by 956 // default will only return already-existing tiles. However, we 957 // need to return *all* tiles to the callers, so force creation 958 // here. 959 //boolean forceTileCreation = true; 960 for (Tile tile : ts.allTilesCreate()) { 961 Image img = getLoadedTileImage(tile); 962 if (img == null || tile.hasError()) { 963 if (Main.isDebugEnabled()) { 964 Main.debug("missed tile: " + tile); 965 } 966 missedTiles.add(tile); 967 continue; 968 } 969 Rectangle sourceRect = tileToRect(tile); 970 if (borderRect != null && !sourceRect.intersects(borderRect)) { 971 continue; 972 } 973 drawImageInside(g, img, sourceRect, borderRect); 974 } 975 return missedTiles; 976 } 977 978 private void myDrawString(Graphics g, String text, int x, int y) { 979 Color oldColor = g.getColor(); 980 g.setColor(Color.black); 981 g.drawString(text,x+1,y+1); 982 g.setColor(oldColor); 983 g.drawString(text,x,y); 984 } 985 986 private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) { 987 int fontHeight = g.getFontMetrics().getHeight(); 988 if (tile == null) 989 return; 990 Point p = pixelPos(t); 991 int texty = p.y + 2 + fontHeight; 992 993 /*if (PROP_DRAW_DEBUG.get()) { 994 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty); 995 texty += 1 + fontHeight; 996 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) { 997 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty); 998 texty += 1 + fontHeight; 999 } 1000 }*/ 1001 1002 /*String tileStatus = tile.getStatus(); 1003 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) { 1004 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty); 1005 texty += 1 + fontHeight; 1006 }*/ 1007 1008 if (tile.hasError() && showErrors) { 1009 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty); 1010 texty += 1 + fontHeight; 1011 } 1012 1013 /*int xCursor = -1; 1014 int yCursor = -1; 1015 if (PROP_DRAW_DEBUG.get()) { 1016 if (yCursor < t.getYtile()) { 1017 if (t.getYtile() % 32 == 31) { 1018 g.fillRect(0, p.y - 1, mv.getWidth(), 3); 1019 } else { 1020 g.drawLine(0, p.y, mv.getWidth(), p.y); 1021 } 1022 yCursor = t.getYtile(); 1023 } 1024 // This draws the vertical lines for the entire 1025 // column. Only draw them for the top tile in 1026 // the column. 1027 if (xCursor < t.getXtile()) { 1028 if (t.getXtile() % 32 == 0) { 1029 // level 7 tile boundary 1030 g.fillRect(p.x - 1, 0, 3, mv.getHeight()); 1031 } else { 1032 g.drawLine(p.x, 0, p.x, mv.getHeight()); 1033 } 1034 xCursor = t.getXtile(); 1035 } 1036 }*/ 1037 } 1038 1039 private Point pixelPos(LatLon ll) { 1040 return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy())); 1041 } 1042 1043 private Point pixelPos(Tile t) { 1044 double lon = tileSource.tileXToLon(t.getXtile(), t.getZoom()); 1045 LatLon tmpLL = new LatLon(tileSource.tileYToLat(t.getYtile(), t.getZoom()), lon); 1046 return pixelPos(tmpLL); 1047 } 1048 1049 private LatLon getShiftedLatLon(EastNorth en) { 1050 return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy())); 1051 } 1052 1053 private Coordinate getShiftedCoord(EastNorth en) { 1054 LatLon ll = getShiftedLatLon(en); 1055 return new Coordinate(ll.lat(),ll.lon()); 1056 } 1057 1058 private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0); 1059 private final class TileSet { 1060 int x0, x1, y0, y1; 1061 int zoom; 1062 int tileMax = -1; 1063 1064 /** 1065 * Create a TileSet by EastNorth bbox taking a layer shift in account 1066 */ 1067 private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) { 1068 this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom); 1069 } 1070 1071 /** 1072 * Create a TileSet by known LatLon bbox without layer shift correction 1073 */ 1074 private TileSet(LatLon topLeft, LatLon botRight, int zoom) { 1075 this.zoom = zoom; 1076 if (zoom == 0) 1077 return; 1078 1079 x0 = (int)tileSource.lonToTileX(topLeft.lon(), zoom); 1080 y0 = (int)tileSource.latToTileY(topLeft.lat(), zoom); 1081 x1 = (int)tileSource.lonToTileX(botRight.lon(), zoom); 1082 y1 = (int)tileSource.latToTileY(botRight.lat(), zoom); 1083 if (x0 > x1) { 1084 int tmp = x0; 1085 x0 = x1; 1086 x1 = tmp; 1087 } 1088 if (y0 > y1) { 1089 int tmp = y0; 1090 y0 = y1; 1091 y1 = tmp; 1092 } 1093 tileMax = (int)Math.pow(2.0, zoom); 1094 if (x0 < 0) { 1095 x0 = 0; 1096 } 1097 if (y0 < 0) { 1098 y0 = 0; 1099 } 1100 if (x1 > tileMax) { 1101 x1 = tileMax; 1102 } 1103 if (y1 > tileMax) { 1104 y1 = tileMax; 1105 } 1106 } 1107 1108 private boolean tooSmall() { 1109 return this.tilesSpanned() < 2.1; 1110 } 1111 1112 private boolean tooLarge() { 1113 return this.tilesSpanned() > 10; 1114 } 1115 1116 private boolean insane() { 1117 return this.tilesSpanned() > 100; 1118 } 1119 1120 private double tilesSpanned() { 1121 return Math.sqrt(1.0 * this.size()); 1122 } 1123 1124 private int size() { 1125 int x_span = x1 - x0 + 1; 1126 int y_span = y1 - y0 + 1; 1127 return x_span * y_span; 1128 } 1129 1130 /* 1131 * Get all tiles represented by this TileSet that are 1132 * already in the tileCache. 1133 */ 1134 private List<Tile> allExistingTiles() { 1135 return this.__allTiles(false); 1136 } 1137 1138 private List<Tile> allTilesCreate() { 1139 return this.__allTiles(true); 1140 } 1141 1142 private List<Tile> __allTiles(boolean create) { 1143 // Tileset is either empty or too large 1144 if (zoom == 0 || this.insane()) 1145 return Collections.emptyList(); 1146 List<Tile> ret = new ArrayList<>(); 1147 for (int x = x0; x <= x1; x++) { 1148 for (int y = y0; y <= y1; y++) { 1149 Tile t; 1150 if (create) { 1151 t = getOrCreateTile(x % tileMax, y % tileMax, zoom); 1152 } else { 1153 t = getTile(x % tileMax, y % tileMax, zoom); 1154 } 1155 if (t != null) { 1156 ret.add(t); 1157 } 1158 } 1159 } 1160 return ret; 1161 } 1162 1163 private List<Tile> allLoadedTiles() { 1164 List<Tile> ret = new ArrayList<>(); 1165 for (Tile t : this.allExistingTiles()) { 1166 if (t.isLoaded()) 1167 ret.add(t); 1168 } 1169 return ret; 1170 } 1171 1172 /** 1173 * @return comparator, that sorts the tiles from the center to the edge of the current screen 1174 */ 1175 private Comparator<Tile> getTileDistanceComparator() { 1176 final int centerX = (int) Math.ceil((x0 + x1) / 2d); 1177 final int centerY = (int) Math.ceil((y0 + y1) / 2d); 1178 return new Comparator<Tile>() { 1179 private int getDistance(Tile t) { 1180 return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY); 1181 } 1182 @Override 1183 public int compare(Tile o1, Tile o2) { 1184 int distance1 = getDistance(o1); 1185 int distance2 = getDistance(o2); 1186 return Integer.compare(distance1, distance2); 1187 } 1188 }; 1189 } 1190 1191 1192 private void loadAllTiles(boolean force) { 1193 if (!autoLoad && !force) 1194 return; 1195 List<Tile> allTiles = allTilesCreate(); 1196 Collections.sort(allTiles, getTileDistanceComparator()); 1197 for (Tile t : allTiles) { 1198 loadTile(t, false); 1199 } 1200 } 1201 1202 private void loadAllErrorTiles(boolean force) { 1203 if (!autoLoad && !force) 1204 return; 1205 for (Tile t : this.allTilesCreate()) { 1206 if (t.hasError()) { 1207 loadTile(t, true); 1208 } 1209 } 1210 } 1211 } 1212 1213 1214 private static class TileSetInfo { 1215 public boolean hasVisibleTiles = false; 1216 public boolean hasOverzoomedTiles = false; 1217 public boolean hasLoadingTiles = false; 1218 } 1219 1220 private static TileSetInfo getTileSetInfo(TileSet ts) { 1221 List<Tile> allTiles = ts.allExistingTiles(); 1222 TileSetInfo result = new TileSetInfo(); 1223 result.hasLoadingTiles = allTiles.size() < ts.size(); 1224 for (Tile t : allTiles) { 1225 if (t.isLoaded()) { 1226 if (!t.hasError()) { 1227 result.hasVisibleTiles = true; 1228 } 1229 if ("no-tile".equals(t.getValue("tile-info"))) { 1230 result.hasOverzoomedTiles = true; 1231 } 1232 } else { 1233 result.hasLoadingTiles = true; 1234 } 1235 } 1236 return result; 1237 } 1238 1239 private class DeepTileSet { 1240 private final EastNorth topLeft, botRight; 1241 private final int minZoom, maxZoom; 1242 private final TileSet[] tileSets; 1243 private final TileSetInfo[] tileSetInfos; 1244 public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) { 1245 this.topLeft = topLeft; 1246 this.botRight = botRight; 1247 this.minZoom = minZoom; 1248 this.maxZoom = maxZoom; 1249 this.tileSets = new TileSet[maxZoom - minZoom + 1]; 1250 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1]; 1251 } 1252 public TileSet getTileSet(int zoom) { 1253 if (zoom < minZoom) 1254 return nullTileSet; 1255 synchronized (tileSets) { 1256 TileSet ts = tileSets[zoom-minZoom]; 1257 if (ts == null) { 1258 ts = new TileSet(topLeft, botRight, zoom); 1259 tileSets[zoom-minZoom] = ts; 1260 } 1261 return ts; 1262 } 1263 } 1264 1265 public TileSetInfo getTileSetInfo(int zoom) { 1266 if (zoom < minZoom) 1267 return new TileSetInfo(); 1268 synchronized (tileSetInfos) { 1269 TileSetInfo tsi = tileSetInfos[zoom-minZoom]; 1270 if (tsi == null) { 1271 tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom)); 1272 tileSetInfos[zoom-minZoom] = tsi; 1273 } 1274 return tsi; 1275 } 1276 } 1277 } 1278 1279 @Override 1280 public void paint(Graphics2D g, MapView mv, Bounds bounds) { 1281 EastNorth topLeft = mv.getEastNorth(0, 0); 1282 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight()); 1283 1284 if (botRight.east() == 0.0 || botRight.north() == 0) { 1285 /*Main.debug("still initializing??");*/ 1286 // probably still initializing 1287 return; 1288 } 1289 1290 needRedraw = false; 1291 1292 int zoom = currentZoomLevel; 1293 if (autoZoom) { 1294 double pixelScaling = getScaleFactor(zoom); 1295 if (pixelScaling > 3 || pixelScaling < 0.7) { 1296 zoom = getBestZoom(); 1297 } 1298 } 1299 1300 DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom); 1301 TileSet ts = dts.getTileSet(zoom); 1302 1303 int displayZoomLevel = zoom; 1304 1305 boolean noTilesAtZoom = false; 1306 if (autoZoom && autoLoad) { 1307 // Auto-detection of tilesource maxzoom (currently fully works only for Bing) 1308 TileSetInfo tsi = dts.getTileSetInfo(zoom); 1309 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) { 1310 noTilesAtZoom = true; 1311 } 1312 // Find highest zoom level with at least one visible tile 1313 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) { 1314 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) { 1315 displayZoomLevel = tmpZoom; 1316 break; 1317 } 1318 } 1319 // Do binary search between currentZoomLevel and displayZoomLevel 1320 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){ 1321 zoom = (zoom + displayZoomLevel)/2; 1322 tsi = dts.getTileSetInfo(zoom); 1323 } 1324 1325 setZoomLevel(zoom); 1326 1327 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level 1328 // to make sure there're really no more zoom levels 1329 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) { 1330 zoom++; 1331 tsi = dts.getTileSetInfo(zoom); 1332 } 1333 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded, 1334 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded. 1335 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) { 1336 zoom--; 1337 tsi = dts.getTileSetInfo(zoom); 1338 } 1339 ts = dts.getTileSet(zoom); 1340 } else if (autoZoom) { 1341 setZoomLevel(zoom); 1342 } 1343 1344 // Too many tiles... refuse to download 1345 if (!ts.tooLarge()) { 1346 //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned()); 1347 ts.loadAllTiles(false); 1348 } 1349 1350 if (displayZoomLevel != zoom) { 1351 ts = dts.getTileSet(displayZoomLevel); 1352 } 1353 1354 g.setColor(Color.DARK_GRAY); 1355 1356 List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null); 1357 int[] otherZooms = { -1, 1, -2, 2, -3, -4, -5}; 1358 for (int zoomOffset : otherZooms) { 1359 if (!autoZoom) { 1360 break; 1361 } 1362 int newzoom = displayZoomLevel + zoomOffset; 1363 if (newzoom < MIN_ZOOM) { 1364 continue; 1365 } 1366 if (missedTiles.isEmpty()) { 1367 break; 1368 } 1369 List<Tile> newlyMissedTiles = new LinkedList<>(); 1370 for (Tile missed : missedTiles) { 1371 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) { 1372 // Don't try to paint from higher zoom levels when tile is overzoomed 1373 newlyMissedTiles.add(missed); 1374 continue; 1375 } 1376 Tile t2 = tempCornerTile(missed); 1377 LatLon topLeft2 = tileLatLon(missed); 1378 LatLon botRight2 = tileLatLon(t2); 1379 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom); 1380 // Instantiating large TileSets is expensive. If there 1381 // are no loaded tiles, don't bother even trying. 1382 if (ts2.allLoadedTiles().isEmpty()) { 1383 newlyMissedTiles.add(missed); 1384 continue; 1385 } 1386 if (ts2.tooLarge()) { 1387 continue; 1388 } 1389 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed)); 1390 } 1391 missedTiles = newlyMissedTiles; 1392 } 1393 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) { 1394 Main.debug("still missed "+missedTiles.size()+" in the end"); 1395 } 1396 g.setColor(Color.red); 1397 g.setFont(InfoFont); 1398 1399 // The current zoom tileset should have all of its tiles 1400 // due to the loadAllTiles(), unless it to tooLarge() 1401 for (Tile t : ts.allExistingTiles()) { 1402 this.paintTileText(ts, t, g, mv, displayZoomLevel, t); 1403 } 1404 1405 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight), displayZoomLevel, this); 1406 1407 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120); 1408 g.setColor(Color.lightGray); 1409 if (!autoZoom) { 1410 if (ts.insane()) { 1411 myDrawString(g, tr("zoom in to load any tiles"), 120, 120); 1412 } else if (ts.tooLarge()) { 1413 myDrawString(g, tr("zoom in to load more tiles"), 120, 120); 1414 } else if (ts.tooSmall()) { 1415 myDrawString(g, tr("increase zoom level to see more detail"), 120, 120); 1416 } 1417 } 1418 if (noTilesAtZoom) { 1419 myDrawString(g, tr("No tiles at this zoom level"), 120, 120); 1420 } 1421 if (Main.isDebugEnabled()) { 1422 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140); 1423 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155); 1424 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170); 1425 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185); 1426 if(tileLoader instanceof TMSCachedTileLoader) { 1427 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader; 1428 int offset = 185; 1429 for(String part: cachedTileLoader.getStats().split("\n")) { 1430 myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15); 1431 } 1432 1433 } 1434 } 1435 } 1436 1437 /** 1438 * This isn't very efficient, but it is only used when the 1439 * user right-clicks on the map. 1440 */ 1441 private Tile getTileForPixelpos(int px, int py) { 1442 if (Main.isDebugEnabled()) { 1443 Main.debug("getTileForPixelpos("+px+", "+py+")"); 1444 } 1445 MapView mv = Main.map.mapView; 1446 Point clicked = new Point(px, py); 1447 EastNorth topLeft = mv.getEastNorth(0, 0); 1448 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight()); 1449 int z = currentZoomLevel; 1450 TileSet ts = new TileSet(topLeft, botRight, z); 1451 1452 if (!ts.tooLarge()) { 1453 ts.loadAllTiles(false); // make sure there are tile objects for all tiles 1454 } 1455 Tile clickedTile = null; 1456 for (Tile t1 : ts.allExistingTiles()) { 1457 Tile t2 = tempCornerTile(t1); 1458 Rectangle r = new Rectangle(pixelPos(t1)); 1459 r.add(pixelPos(t2)); 1460 if (Main.isDebugEnabled()) { 1461 Main.debug("r: " + r + " clicked: " + clicked); 1462 } 1463 if (!r.contains(clicked)) { 1464 continue; 1465 } 1466 clickedTile = t1; 1467 break; 1468 } 1469 if (clickedTile == null) 1470 return null; 1471 /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() + 1472 " currentZoomLevel: " + currentZoomLevel);*/ 1473 return clickedTile; 1474 } 1475 1476 @Override 1477 public Action[] getMenuEntries() { 1478 return new Action[] { 1479 LayerListDialog.getInstance().createActivateLayerAction(this), 1480 LayerListDialog.getInstance().createShowHideLayerAction(), 1481 LayerListDialog.getInstance().createDeleteLayerAction(), 1482 SeparatorLayerAction.INSTANCE, 1483 // color, 1484 new OffsetAction(), 1485 new RenameLayerAction(this.getAssociatedFile(), this), 1486 SeparatorLayerAction.INSTANCE, 1487 new AutoLoadTilesAction(), 1488 new AutoZoomAction(), 1489 new ZoomToBestAction(), 1490 new ZoomToNativeLevelAction(), 1491 new LoadErroneusTilesAction(), 1492 new LoadAllTilesAction(), 1493 new LayerListPopup.InfoAction(this) 1494 }; 1495 } 1496 1497 @Override 1498 public String getToolTipText() { 1499 if(autoLoad) { 1500 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); 1501 } else { 1502 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); 1503 } 1504 } 1505 1506 @Override 1507 public void visitBoundingBox(BoundingXYVisitor v) { 1508 } 1509 1510 @Override 1511 public boolean isChanged() { 1512 return needRedraw; 1513 } 1514 1515 /** 1516 * Task responsible for precaching imagery along the gpx track 1517 * 1518 */ 1519 public class PrecacheTask implements TileLoaderListener { 1520 private final ProgressMonitor progressMonitor; 1521 private volatile int totalCount; 1522 private volatile int processedCount = 0; 1523 private TileLoader tileLoader; 1524 1525 /** 1526 * @param progressMonitor that will be notified about progess of the task 1527 */ 1528 public PrecacheTask(ProgressMonitor progressMonitor) { 1529 this.progressMonitor = progressMonitor; 1530 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource)); 1531 if (this.tileLoader instanceof TMSCachedTileLoader) { 1532 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor( 1533 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader")); 1534 } 1535 1536 } 1537 1538 /** 1539 * @return true, if all is done 1540 */ 1541 public boolean isFinished() { 1542 return processedCount >= totalCount; 1543 } 1544 1545 /** 1546 * @return total number of tiles to download 1547 */ 1548 public int getTotalCount() { 1549 return totalCount; 1550 } 1551 1552 /** 1553 * cancel the task 1554 */ 1555 public void cancel() { 1556 if (tileLoader instanceof TMSCachedTileLoader) { 1557 ((TMSCachedTileLoader)tileLoader).cancelOutstandingTasks(); 1558 } 1559 } 1560 1561 1562 @Override 1563 public void tileLoadingFinished(Tile tile, boolean success) { 1564 if (success) { 1565 this.processedCount++; 1566 this.progressMonitor.worked(1); 1567 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processedCount, totalCount)); 1568 } 1569 } 1570 1571 /** 1572 * @return tile loader that is used to load the tiles 1573 */ 1574 public TileLoader getTileLoader() { 1575 return tileLoader; 1576 } 1577 } 1578 1579 /** 1580 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download 1581 * all of the tiles. Buffer contains at least one tile. 1582 * 1583 * To prevent accidental clear of the queue, new download executor is created with separate queue 1584 * 1585 * @param precacheTask 1586 * @param points 1587 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides 1588 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides 1589 */ 1590 public void downloadAreaToCache(final PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) { 1591 final Set<Tile> requestedTiles = new ConcurrentSkipListSet <>(new Comparator<Tile>() { 1592 public int compare(Tile o1, Tile o2) { 1593 return String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey()); 1594 } 1595 }); 1596 for (LatLon point: points) { 1597 int minY = (int) Math.round(tileSource.latToTileY(point.lat() - bufferY, currentZoomLevel)); 1598 int curY = (int) Math.round(tileSource.latToTileY(point.lat(), currentZoomLevel)); 1599 int maxY = (int) Math.round(tileSource.latToTileY(point.lat() + bufferY, currentZoomLevel)); 1600 int minX = (int) Math.round(tileSource.lonToTileX(point.lon() - bufferX, currentZoomLevel)); 1601 int curX = (int) Math.round(tileSource.lonToTileX(point.lon(), currentZoomLevel)); 1602 int maxX = (int) Math.round(tileSource.lonToTileX(point.lon() + bufferX, currentZoomLevel)); 1603 1604 // take at least one tile of buffer 1605 minY = Math.min(curY - 1, minY); 1606 maxY = Math.max(curY + 1, maxY); 1607 minX = Math.min(curX - 1, minX); 1608 maxX = Math.min(curX + 1, maxX); 1609 1610 for (int x= minX; x<=maxX; x++) { 1611 for (int y= minY; y<=maxY; y++) { 1612 requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel)); 1613 } 1614 } 1615 } 1616 1617 precacheTask.totalCount = requestedTiles.size(); 1618 precacheTask.progressMonitor.setTicksCount(requestedTiles.size()); 1619 1620 TileLoader loader = precacheTask.getTileLoader(); 1621 for (Tile t: requestedTiles) { 1622 loader.createTileLoaderJob(t).submit(); 1623 } 1624 } 1625 1626 @Override 1627 public boolean isSavable() { 1628 return true; // With WMSLayerExporter 1629 } 1630 1631 @Override 1632 public File createAndOpenSaveFileChooser() { 1633 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 1634 } 1635 } -
src/org/openstreetmap/josm/gui/layer/TMSLayer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/TMSLayer.java b/src/org/openstreetmap/josm/gui/layer/TMSLayer.java index 9201712..a539567 100644
a b package org.openstreetmap.josm.gui.layer; 3 3 4 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 5 6 import java.awt.Color;7 import java.awt.Font;8 import java.awt.Graphics;9 import java.awt.Graphics2D;10 import java.awt.GridBagLayout;11 import java.awt.Image;12 import java.awt.Point;13 import java.awt.Rectangle;14 import java.awt.Toolkit;15 import java.awt.event.ActionEvent;16 import java.awt.event.MouseAdapter;17 import java.awt.event.MouseEvent;18 import java.awt.image.ImageObserver;19 import java.io.File;20 6 import java.io.IOException; 21 import java.io.StringReader;22 import java.net.URL;23 import java.text.SimpleDateFormat;24 import java.util.ArrayList;25 import java.util.Collections;26 import java.util.Comparator;27 import java.util.Date;28 import java.util.HashMap;29 import java.util.LinkedList;30 import java.util.List;31 7 import java.util.Map; 32 import java.util.Map.Entry;33 import java.util.Scanner;34 import java.util.concurrent.Callable;35 import java.util.regex.Matcher;36 import java.util.regex.Pattern;37 8 38 import javax.swing.AbstractAction;39 import javax.swing.Action;40 import javax.swing.BorderFactory;41 import javax.swing.JCheckBoxMenuItem;42 import javax.swing.JLabel;43 import javax.swing.JMenuItem;44 import javax.swing.JOptionPane;45 import javax.swing.JPanel;46 import javax.swing.JPopupMenu;47 import javax.swing.JTextField;48 49 import org.openstreetmap.gui.jmapviewer.AttributionSupport;50 import org.openstreetmap.gui.jmapviewer.Coordinate;51 import org.openstreetmap.gui.jmapviewer.MemoryTileCache;52 import org.openstreetmap.gui.jmapviewer.OsmTileLoader;53 import org.openstreetmap.gui.jmapviewer.Tile;54 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;55 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;56 9 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 57 10 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 58 11 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 59 import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource; 60 import org.openstreetmap.gui.jmapviewer.tilesources.ScanexTileSource; 61 import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource; 12 import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; 62 13 import org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource; 63 import org.openstreetmap.josm.Main; 64 import org.openstreetmap.josm.actions.RenameLayerAction; 65 import org.openstreetmap.josm.data.Bounds; 66 import org.openstreetmap.josm.data.Version; 67 import org.openstreetmap.josm.data.coor.EastNorth; 68 import org.openstreetmap.josm.data.coor.LatLon; 14 import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory; 69 15 import org.openstreetmap.josm.data.imagery.ImageryInfo; 70 16 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 71 17 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 72 import org.openstreetmap.josm.data. osm.visitor.BoundingXYVisitor;18 import org.openstreetmap.josm.data.imagery.TileLoaderFactory; 73 19 import org.openstreetmap.josm.data.preferences.BooleanProperty; 74 20 import org.openstreetmap.josm.data.preferences.IntegerProperty; 75 import org.openstreetmap.josm.data.preferences.StringProperty;76 21 import org.openstreetmap.josm.data.projection.Projection; 77 import org.openstreetmap.josm.gui.ExtendedDialog; 78 import org.openstreetmap.josm.gui.MapFrame; 79 import org.openstreetmap.josm.gui.MapView; 80 import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 81 import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; 82 import org.openstreetmap.josm.gui.PleaseWaitRunnable; 83 import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 84 import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 85 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 86 import org.openstreetmap.josm.io.CacheCustomContent; 87 import org.openstreetmap.josm.io.OsmTransferException; 88 import org.openstreetmap.josm.io.UTFInputStreamReader; 89 import org.openstreetmap.josm.tools.CheckParameterUtil; 90 import org.openstreetmap.josm.tools.GBC; 91 import org.openstreetmap.josm.tools.Utils; 92 import org.xml.sax.InputSource; 93 import org.xml.sax.SAXException; 22 94 23 95 24 /** 96 25 * Class that displays a slippy map layer. … … import org.xml.sax.SAXException; 101 30 * @author Upliner <upliner@gmail.com> 102 31 * 103 32 */ 104 public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener { 105 public static final String PREFERENCE_PREFIX = "imagery.tms"; 106 107 public static final int MAX_ZOOM = 30; 108 public static final int MIN_ZOOM = 2; 109 public static final int DEFAULT_MAX_ZOOM = 20; 110 public static final int DEFAULT_MIN_ZOOM = 2; 111 112 public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true); 113 public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true); 114 public static final BooleanProperty PROP_DEFAULT_SHOWERRORS = new BooleanProperty(PREFERENCE_PREFIX + ".default_showerrors", true); 115 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", DEFAULT_MIN_ZOOM); 116 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM); 117 public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + 118 ".add_to_slippymap_chooser", true); 119 public static final StringProperty PROP_TILECACHE_DIR; 33 public class TMSLayer extends AbstractTileSourceLayer { 34 private static final String PREFERENCE_PREFIX = "imagery.tms"; 120 35 121 static { 122 String defPath = null; 123 try { 124 defPath = new File(Main.pref.getCacheDirectory(), "tms").getAbsolutePath(); 125 } catch (SecurityException e) { 126 Main.warn(e); 127 } 128 PROP_TILECACHE_DIR = new StringProperty(PREFERENCE_PREFIX + ".tilecache", defPath); 129 } 36 /** minimum zoom level for TMS layer */ 37 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", AbstractTileSourceLayer.PROP_MIN_ZOOM_LVL.get()); 38 /** maximum zoom level for TMS layer */ 39 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", AbstractTileSourceLayer.PROP_MAX_ZOOM_LVL.get()); 40 /** shall TMS layers be added to download dialog */ 41 public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true); 130 42 131 /** 132 * Interface for creating TileLoaders, ie. classes responsible for loading tiles on map 133 * 134 */ 135 public interface TileLoaderFactory { 136 /** 137 * @param listener object that will be notified, when tile has finished loading 138 * @return TileLoader that will notify the listener 139 */ 140 TileLoader makeTileLoader(TileLoaderListener listener); 141 142 /** 143 * @param listener object that will be notified, when tile has finished loading 144 * @param headers HTTP headers that should be sent by TileLoader to tile server 145 * @return TileLoader that will notify the listener 146 */ 147 TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers); 148 } 43 /** loader factory responsible for loading tiles for this layer */ 44 public static TileLoaderFactory loaderFactory = new CachedTileLoaderFactory("TMS"){ 149 45 150 protected TileCache tileCache;151 protected TileSource tileSource;152 protected TileLoader tileLoader;153 154 155 public static TileLoaderFactory loaderFactory = new TileLoaderFactory() {156 46 @Override 157 public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) { 158 Map<String, String> headers = new HashMap<>(); 159 headers.put("User-Agent", Version.getInstance().getFullAgentString()); 160 headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*"); 161 if (inputHeaders != null) 162 headers.putAll(inputHeaders); 163 164 try { 165 return new TMSCachedTileLoader(listener, "TMS", 166 Main.pref.getInteger("socket.timeout.connect",15) * 1000, 167 Main.pref.getInteger("socket.timeout.read", 30) * 1000, 168 headers, 169 PROP_TILECACHE_DIR.get()); 170 } catch (IOException e) { 171 Main.warn(e); 172 } 173 return null; 47 protected TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout, 48 int readTimeout, Map<String, String> headers, String cacheDir) throws IOException { 49 return new TMSCachedTileLoader(listener, cacheName, connectTimeout, readTimeout, headers, cacheDir); 174 50 } 175 51 176 @Override177 public TileLoader makeTileLoader(TileLoaderListener listener) {178 return makeTileLoader(listener, null);179 }180 52 }; 181 53 182 54 /** 183 * Plugins that wish to set custom tile loader should call this method 55 * Create a layer based on ImageryInfo 56 * @param info description of the layer 184 57 */ 185 186 public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) { 187 TMSLayer.loaderFactory = loaderFactory; 188 } 189 190 @Override 191 public synchronized void tileLoadingFinished(Tile tile, boolean success) { 192 if (tile.hasError()) { 193 success = false; 194 tile.setImage(null); 195 } 196 if (sharpenLevel != 0 && success) { 197 tile.setImage(sharpenImage(tile.getImage())); 198 } 199 tile.setLoaded(success); 200 needRedraw = true; 201 if (Main.map != null) { 202 Main.map.repaint(100); 203 } 204 if (Main.isDebugEnabled()) { 205 Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success); 206 } 207 } 208 209 /** 210 * Clears the tile cache. 211 * 212 * If the current tileLoader is an instance of OsmTileLoader, a new 213 * TmsTileClearController is created and passed to the according clearCache 214 * method. 215 * 216 * @param monitor not used in this implementation - as cache clear is instaneus 217 */ 218 public void clearTileCache(ProgressMonitor monitor) { 219 tileCache.clear(); 220 if (tileLoader instanceof CachedTileLoader) { 221 ((CachedTileLoader)tileLoader).clearCache(tileSource); 222 } 223 redraw(); 58 public TMSLayer(ImageryInfo info) { 59 super(info); 224 60 } 225 61 226 62 /** 227 * Zoomlevel at which tiles is currently downloaded. 228 * Initial zoom lvl is set to bestZoom 229 */ 230 public int currentZoomLevel; 231 232 private Tile clickedTile; 233 private boolean needRedraw; 234 private JPopupMenu tileOptionMenu; 235 private JCheckBoxMenuItem autoZoomPopup; 236 private JCheckBoxMenuItem autoLoadPopup; 237 private JCheckBoxMenuItem showErrorsPopup; 238 private Tile showMetadataTile; 239 private AttributionSupport attribution = new AttributionSupport(); 240 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13); 241 242 protected boolean autoZoom; 243 protected boolean autoLoad; 244 protected boolean showErrors; 245 246 /** 247 * Initiates a repaint of Main.map 248 * 249 * @see Main#map 250 * @see MapFrame#repaint() 63 * Plugins that wish to set custom tile loader should call this method 64 * @param newLoaderFactory that will be used to load tiles 251 65 */ 252 protected void redraw() {253 needRedraw = true;254 Main.map.repaint();255 }256 257 protected static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {258 if(maxZoomLvl > MAX_ZOOM) {259 maxZoomLvl = MAX_ZOOM;260 }261 if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {262 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();263 }264 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {265 maxZoomLvl = ts.getMaxZoom();266 }267 return maxZoomLvl;268 }269 270 public static int getMaxZoomLvl(TileSource ts) {271 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);272 }273 274 public static void setMaxZoomLvl(int maxZoomLvl) {275 Integer newMaxZoom = Integer.valueOf(checkMaxZoomLvl(maxZoomLvl, null));276 PROP_MAX_ZOOM_LVL.put(newMaxZoom);277 }278 66 279 static int checkMinZoomLvl(int minZoomLvl, TileSource ts) { 280 if(minZoomLvl < MIN_ZOOM) { 281 /*Main.debug("Min. zoom level should not be less than "+MIN_ZOOM+"! Setting to that.");*/ 282 minZoomLvl = MIN_ZOOM; 283 } 284 if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) { 285 /*Main.debug("Min. zoom level should not be more than Max. zoom level! Setting to Max.");*/ 286 minZoomLvl = getMaxZoomLvl(ts); 287 } 288 if (ts != null && ts.getMinZoom() > minZoomLvl) { 289 /*Main.debug("Increasing min. zoom level to match tile source");*/ 290 minZoomLvl = ts.getMinZoom(); 291 } 292 return minZoomLvl; 293 } 294 295 public static int getMinZoomLvl(TileSource ts) { 296 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts); 297 } 298 299 public static void setMinZoomLvl(int minZoomLvl) { 300 minZoomLvl = checkMinZoomLvl(minZoomLvl, null); 301 PROP_MIN_ZOOM_LVL.put(minZoomLvl); 302 } 303 304 private static class CachedAttributionBingAerialTileSource extends BingAerialTileSource { 305 306 public CachedAttributionBingAerialTileSource(ImageryInfo info) { 307 super(info); 308 } 309 310 class BingAttributionData extends CacheCustomContent<IOException> { 311 312 public BingAttributionData() { 313 super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY); 67 public static void setTileLoaderFactory(TileLoaderFactory newLoaderFactory) { 68 loaderFactory = newLoaderFactory; 314 69 } 315 70 316 71 @Override 317 protected byte[] updateData() throws IOException { 318 URL u = getAttributionUrl(); 319 try (Scanner scanner = new Scanner(UTFInputStreamReader.create(Utils.openURL(u)))) { 320 String r = scanner.useDelimiter("\\A").next(); 321 Main.info("Successfully loaded Bing attribution data."); 322 return r.getBytes("UTF-8"); 323 } 72 protected TileLoaderFactory getTileLoaderFactory() { 73 return loaderFactory; 324 74 } 325 }326 327 @Override328 protected Callable<List<Attribution>> getAttributionLoaderCallable() {329 return new Callable<List<Attribution>>() {330 75 331 76 @Override 332 public List<Attribution> call() throws Exception { 333 BingAttributionData attributionLoader = new BingAttributionData(); 334 int waitTimeSec = 1; 335 while (true) { 336 try { 337 String xml = attributionLoader.updateIfRequiredString(); 338 return parseAttributionText(new InputSource(new StringReader(xml))); 339 } catch (IOException ex) { 340 Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds."); 341 Thread.sleep(waitTimeSec * 1000L); 342 waitTimeSec *= 2; 343 } 344 } 345 } 346 }; 77 protected Map<String, String> getHeaders(TileSource tileSource) { 78 if (tileSource instanceof TemplatedTMSTileSource) { 79 return ((TemplatedTMSTileSource)tileSource).getHeaders(); 347 80 } 81 return null; 348 82 } 349 83 350 84 /** … … public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderL 359 93 * @return a new TileSource instance or null if no TileSource for the ImageryInfo/ImageryType could be found. 360 94 * @throws IllegalArgumentException 361 95 */ 362 public static TileSource getTileSource(ImageryInfo info) {363 if (info.getImageryType() == ImageryType.TMS) {364 checkUrl(info.getUrl());365 TMSTileSource t = new TemplatedTMSTileSource(info);366 info.setAttribution(t);367 return t;368 } else if (info.getImageryType() == ImageryType.BING) {369 return new CachedAttributionBingAerialTileSource(info);370 } else if (info.getImageryType() == ImageryType.SCANEX) {371 return new ScanexTileSource(info);372 }373 return null;374 }375 376 /**377 * Checks validity of given URL.378 * @param url URL to check379 * @throws IllegalArgumentException if url is null or invalid380 */381 public static void checkUrl(String url) {382 CheckParameterUtil.ensureParameterNotNull(url, "url");383 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);384 while (m.find()) {385 boolean isSupportedPattern = false;386 for (String pattern : TemplatedTMSTileSource.ALL_PATTERNS) {387 if (m.group().matches(pattern)) {388 isSupportedPattern = true;389 break;390 }391 }392 if (!isSupportedPattern) {393 throw new IllegalArgumentException(394 tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url));395 }396 }397 }398 399 private void initTileSource(TileSource tileSource) {400 this.tileSource = tileSource;401 attribution.initialize(tileSource);402 403 currentZoomLevel = getBestZoom();404 405 Map<String, String> headers = null;406 if (tileSource instanceof TemplatedTMSTileSource) {407 headers = (((TemplatedTMSTileSource)tileSource).getHeaders());408 }409 410 tileLoader = loaderFactory.makeTileLoader(this, headers);411 if (tileLoader instanceof TMSCachedTileLoader) {412 tileCache = (TileCache) tileLoader;413 } else {414 tileCache = new MemoryTileCache();415 }416 if (tileLoader == null)417 tileLoader = new OsmTileLoader(this);418 }419 420 /**421 * Marks layer as needing redraw on offset change422 */423 96 @Override 424 public void setOffset(double dx, double dy) { 425 super.setOffset(dx, dy); 426 needRedraw = true; 427 } 428 /** 429 * Returns average number of screen pixels per tile pixel for current mapview 430 */ 431 private double getScaleFactor(int zoom) { 432 if (!Main.isDisplayingMapView()) return 1; 433 MapView mv = Main.map.mapView; 434 LatLon topLeft = mv.getLatLon(0, 0); 435 LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight()); 436 double x1 = tileSource.lonToTileX(topLeft.lon(), zoom); 437 double y1 = tileSource.latToTileY(topLeft.lat(), zoom); 438 double x2 = tileSource.lonToTileX(botRight.lon(), zoom); 439 double y2 = tileSource.latToTileY(botRight.lat(), zoom); 440 441 int screenPixels = mv.getWidth()*mv.getHeight(); 442 double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize()); 443 if (screenPixels == 0 || tilePixels == 0) return 1; 444 return screenPixels/tilePixels; 445 } 446 447 private final int getBestZoom() { 448 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view 449 double result = Math.log(factor)/Math.log(2)/2+1; 450 /* 451 * Math.log(factor)/Math.log(2) - gives log base 2 of factor 452 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2 453 * In general, smaller zoom levels are more readable. We prefer big, 454 * block, pixelated (but readable) map text to small, smeared, 455 * unreadable underzoomed text. So, use .floor() instead of rounding 456 * to skew things a bit toward the lower zooms. 457 * Remember, that result here, should correspond to TMSLayer.paint(...) 458 * getScaleFactor(...) is supposed to be between 0.75 and 3 459 */ 460 int intResult = (int)Math.floor(result); 461 if (intResult > getMaxZoomLvl()) 462 return getMaxZoomLvl(); 463 if (intResult < getMinZoomLvl()) 464 return getMinZoomLvl(); 465 return intResult; 466 } 467 468 @SuppressWarnings("serial") 469 public TMSLayer(ImageryInfo info) { 470 super(info); 471 472 if(!isProjectionSupported(Main.getProjection())) { 473 JOptionPane.showMessageDialog(Main.parent, 474 tr("TMS layers do not support the projection {0}.\n{1}\n" 475 + "Change the projection or remove the layer.", 476 Main.getProjection().toCode(), nameSupportedProjections()), 477 tr("Warning"), 478 JOptionPane.WARNING_MESSAGE); 97 protected TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException { 98 return AbstractTMSTileSource.getTileSource(info); 479 99 } 480 100 481 setBackgroundLayer(true);482 this.setVisible(true);483 484 TileSource source = getTileSource(info);485 if (source == null)486 throw new IllegalStateException("Cannot create TMSLayer with non-TMS ImageryInfo");487 initTileSource(source);488 489 MapView.addZoomChangeListener(this);490 }491 101 492 102 /** 493 103 * Adds a context menu to the mapView. 494 104 */ 495 @Override496 public void hookUpMapView() {497 tileOptionMenu = new JPopupMenu();498 499 autoZoom = PROP_DEFAULT_AUTOZOOM.get();500 autoZoomPopup = new JCheckBoxMenuItem();501 autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {502 @Override503 public void actionPerformed(ActionEvent ae) {504 autoZoom = !autoZoom;505 }506 });507 autoZoomPopup.setSelected(autoZoom);508 tileOptionMenu.add(autoZoomPopup);509 510 autoLoad = PROP_DEFAULT_AUTOLOAD.get();511 autoLoadPopup = new JCheckBoxMenuItem();512 autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {513 @Override514 public void actionPerformed(ActionEvent ae) {515 autoLoad= !autoLoad;516 }517 });518 autoLoadPopup.setSelected(autoLoad);519 tileOptionMenu.add(autoLoadPopup);520 521 showErrors = PROP_DEFAULT_SHOWERRORS.get();522 showErrorsPopup = new JCheckBoxMenuItem();523 showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {524 @Override525 public void actionPerformed(ActionEvent ae) {526 showErrors = !showErrors;527 }528 });529 showErrorsPopup.setSelected(showErrors);530 tileOptionMenu.add(showErrorsPopup);531 532 tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {533 @Override534 public void actionPerformed(ActionEvent ae) {535 if (clickedTile != null) {536 loadTile(clickedTile, true);537 redraw();538 }539 }540 }));541 542 tileOptionMenu.add(new JMenuItem(new AbstractAction(543 tr("Show Tile Info")) {544 private String getSizeString(int size) {545 StringBuilder ret = new StringBuilder();546 return ret.append(size).append("x").append(size).toString();547 }548 549 private JTextField createTextField(String text) {550 JTextField ret = new JTextField(text);551 ret.setEditable(false);552 ret.setBorder(BorderFactory.createEmptyBorder());553 return ret;554 }555 @Override556 public void actionPerformed(ActionEvent ae) {557 if (clickedTile != null) {558 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});559 JPanel panel = new JPanel(new GridBagLayout());560 Rectangle displaySize = tileToRect(clickedTile);561 String url = "";562 try {563 url = clickedTile.getUrl();564 } catch (IOException e) {565 // silence exceptions566 }567 568 String[][] content = {569 {"Tile name", clickedTile.getKey()},570 {"Tile url", url},571 {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },572 {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()},573 };574 575 for (String[] entry: content) {576 panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std());577 panel.add(GBC.glue(5,0), GBC.std());578 panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));579 }580 581 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {582 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std());583 panel.add(GBC.glue(5,0), GBC.std());584 String value = e.getValue();585 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {586 value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));587 }588 panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));589 590 }591 ed.setIcon(JOptionPane.INFORMATION_MESSAGE);592 ed.setContent(panel);593 ed.showDialog();594 }595 }596 }));597 598 tileOptionMenu.add(new JMenuItem(new AbstractAction(599 tr("Request Update")) {600 @Override601 public void actionPerformed(ActionEvent ae) {602 if (clickedTile != null) {603 clickedTile.setLoaded(false);604 tileLoader.createTileLoaderJob(clickedTile).submit();605 }606 }607 }));608 609 tileOptionMenu.add(new JMenuItem(new AbstractAction(610 tr("Load All Tiles")) {611 @Override612 public void actionPerformed(ActionEvent ae) {613 loadAllTiles(true);614 redraw();615 }616 }));617 618 tileOptionMenu.add(new JMenuItem(new AbstractAction(619 tr("Load All Error Tiles")) {620 @Override621 public void actionPerformed(ActionEvent ae) {622 loadAllErrorTiles(true);623 redraw();624 }625 }));626 627 // increase and decrease commands628 tileOptionMenu.add(new JMenuItem(new AbstractAction(629 tr("Increase zoom")) {630 @Override631 public void actionPerformed(ActionEvent ae) {632 increaseZoomLevel();633 redraw();634 }635 }));636 637 tileOptionMenu.add(new JMenuItem(new AbstractAction(638 tr("Decrease zoom")) {639 @Override640 public void actionPerformed(ActionEvent ae) {641 decreaseZoomLevel();642 redraw();643 }644 }));645 646 tileOptionMenu.add(new JMenuItem(new AbstractAction(647 tr("Snap to tile size")) {648 @Override649 public void actionPerformed(ActionEvent ae) {650 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));651 Main.map.mapView.zoomToFactor(newFactor);652 redraw();653 }654 }));655 656 tileOptionMenu.add(new JMenuItem(new AbstractAction(657 tr("Flush Tile Cache")) {658 @Override659 public void actionPerformed(ActionEvent ae) {660 new PleaseWaitRunnable(tr("Flush Tile Cache")) {661 @Override662 protected void realRun() throws SAXException, IOException,663 OsmTransferException {664 clearTileCache(getProgressMonitor());665 }666 667 @Override668 protected void finish() {669 }670 671 @Override672 protected void cancel() {673 }674 }.run();675 }676 }));677 678 final MouseAdapter adapter = new MouseAdapter() {679 @Override680 public void mouseClicked(MouseEvent e) {681 if (!isVisible()) return;682 if (e.getButton() == MouseEvent.BUTTON3) {683 clickedTile = getTileForPixelpos(e.getX(), e.getY());684 tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());685 } else if (e.getButton() == MouseEvent.BUTTON1) {686 attribution.handleAttribution(e.getPoint(), true);687 }688 }689 };690 Main.map.mapView.addMouseListener(adapter);691 692 MapView.addLayerChangeListener(new LayerChangeListener() {693 @Override694 public void activeLayerChange(Layer oldLayer, Layer newLayer) {695 //696 }697 698 @Override699 public void layerAdded(Layer newLayer) {700 //701 }702 703 @Override704 public void layerRemoved(Layer oldLayer) {705 if (oldLayer == TMSLayer.this) {706 Main.map.mapView.removeMouseListener(adapter);707 MapView.removeZoomChangeListener(TMSLayer.this);708 MapView.removeLayerChangeListener(this);709 }710 }711 });712 }713 714 /**715 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all716 * changes to visible map (panning/zooming)717 */718 @Override719 public void zoomChanged() {720 if (Main.isDebugEnabled()) {721 Main.debug("zoomChanged(): " + currentZoomLevel);722 }723 needRedraw = true;724 if (tileLoader instanceof TMSCachedTileLoader) {725 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();726 }727 }728 729 protected int getMaxZoomLvl() {730 if (info.getMaxZoom() != 0)731 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);732 else733 return getMaxZoomLvl(tileSource);734 }735 736 protected int getMinZoomLvl() {737 return getMinZoomLvl(tileSource);738 }739 740 /**741 * Zoom in, go closer to map.742 *743 * @return true, if zoom increasing was successful, false otherwise744 */745 public boolean zoomIncreaseAllowed() {746 boolean zia = currentZoomLevel < this.getMaxZoomLvl();747 if (Main.isDebugEnabled()) {748 Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );749 }750 return zia;751 }752 753 public boolean increaseZoomLevel() {754 if (zoomIncreaseAllowed()) {755 currentZoomLevel++;756 if (Main.isDebugEnabled()) {757 Main.debug("increasing zoom level to: " + currentZoomLevel);758 }759 zoomChanged();760 } else {761 Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+762 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");763 return false;764 }765 return true;766 }767 768 public boolean setZoomLevel(int zoom) {769 if (zoom == currentZoomLevel) return true;770 if (zoom > this.getMaxZoomLvl()) return false;771 if (zoom < this.getMinZoomLvl()) return false;772 currentZoomLevel = zoom;773 zoomChanged();774 return true;775 }776 777 /**778 * Check if zooming out is allowed779 *780 * @return true, if zooming out is allowed (currentZoomLevel > minZoomLevel)781 */782 public boolean zoomDecreaseAllowed() {783 return currentZoomLevel > this.getMinZoomLvl();784 }785 786 /**787 * Zoom out from map.788 *789 * @return true, if zoom increasing was successfull, false othervise790 */791 public boolean decreaseZoomLevel() {792 //int minZoom = this.getMinZoomLvl();793 if (zoomDecreaseAllowed()) {794 if (Main.isDebugEnabled()) {795 Main.debug("decreasing zoom level to: " + currentZoomLevel);796 }797 currentZoomLevel--;798 zoomChanged();799 } else {800 /*Main.debug("Current zoom level could not be decreased. Min. zoom level "+minZoom+" reached.");*/801 return false;802 }803 return true;804 }805 806 /*807 * We use these for quick, hackish calculations. They808 * are temporary only and intentionally not inserted809 * into the tileCache.810 */811 private Tile tempCornerTile(Tile t) {812 int x = t.getXtile() + 1;813 int y = t.getYtile() + 1;814 int zoom = t.getZoom();815 Tile tile = getTile(x, y, zoom);816 if (tile != null)817 return tile;818 return new Tile(tileSource, x, y, zoom);819 }820 821 private Tile getOrCreateTile(int x, int y, int zoom) {822 Tile tile = getTile(x, y, zoom);823 if (tile == null) {824 tile = new Tile(tileSource, x, y, zoom);825 tileCache.addTile(tile);826 tile.loadPlaceholderFromCache(tileCache);827 }828 return tile;829 }830 831 /*832 * This can and will return null for tiles that are not833 * already in the cache.834 */835 private Tile getTile(int x, int y, int zoom) {836 int max = 1 << zoom;837 if (x < 0 || x >= max || y < 0 || y >= max)838 return null;839 return tileCache.getTile(tileSource, x, y, zoom);840 }841 842 private boolean loadTile(Tile tile, boolean force) {843 if (tile == null)844 return false;845 if (!force && (tile.isLoaded() || tile.hasError()))846 return false;847 if (tile.isLoading())848 return false;849 tileLoader.createTileLoaderJob(tile).submit();850 return true;851 }852 853 private void loadAllTiles(boolean force) {854 MapView mv = Main.map.mapView;855 EastNorth topLeft = mv.getEastNorth(0, 0);856 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());857 858 TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);859 860 // if there is more than 18 tiles on screen in any direction, do not861 // load all tiles!862 if (ts.tooLarge()) {863 Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");864 return;865 }866 ts.loadAllTiles(force);867 }868 869 private void loadAllErrorTiles(boolean force) {870 MapView mv = Main.map.mapView;871 EastNorth topLeft = mv.getEastNorth(0, 0);872 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());873 874 TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);875 876 ts.loadAllErrorTiles(force);877 }878 879 @Override880 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {881 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;882 needRedraw = true;883 if (Main.isDebugEnabled()) {884 Main.debug("imageUpdate() done: " + done + " calling repaint");885 }886 Main.map.repaint(done ? 0 : 100);887 return !done;888 }889 890 private boolean imageLoaded(Image i) {891 if (i == null)892 return false;893 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);894 if ((status & ALLBITS) != 0)895 return true;896 return false;897 }898 899 /**900 * Returns the image for the given tile if both tile and image are loaded.901 * Otherwise returns null.902 *903 * @param tile the Tile for which the image should be returned904 * @return the image of the tile or null.905 */906 private Image getLoadedTileImage(Tile tile) {907 if (!tile.isLoaded())908 return null;909 Image img = tile.getImage();910 if (!imageLoaded(img))911 return null;912 return img;913 }914 915 private LatLon tileLatLon(Tile t) {916 int zoom = t.getZoom();917 return new LatLon(tileSource.tileYToLat(t.getYtile(), zoom),918 tileSource.tileXToLon(t.getXtile(), zoom));919 }920 921 private Rectangle tileToRect(Tile t1) {922 /*923 * We need to get a box in which to draw, so advance by one tile in924 * each direction to find the other corner of the box.925 * Note: this somewhat pollutes the tile cache926 */927 Tile t2 = tempCornerTile(t1);928 Rectangle rect = new Rectangle(pixelPos(t1));929 rect.add(pixelPos(t2));930 return rect;931 }932 933 // 'source' is the pixel coordinates for the area that934 // the img is capable of filling in. However, we probably935 // only want a portion of it.936 //937 // 'border' is the screen cordinates that need to be drawn.938 // We must not draw outside of it.939 private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) {940 Rectangle target = source;941 942 // If a border is specified, only draw the intersection943 // if what we have combined with what we are supposed944 // to draw.945 if (border != null) {946 target = source.intersection(border);947 if (Main.isDebugEnabled()) {948 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);949 }950 }951 952 // All of the rectangles are in screen coordinates. We need953 // to how these correlate to the sourceImg pixels. We could954 // avoid doing this by scaling the image up to the 'source' size,955 // but this should be cheaper.956 //957 // In some projections, x any y are scaled differently enough to958 // cause a pixel or two of fudge. Calculate them separately.959 double imageYScaling = sourceImg.getHeight(this) / source.getHeight();960 double imageXScaling = sourceImg.getWidth(this) / source.getWidth();961 962 // How many pixels into the 'source' rectangle are we drawing?963 int screen_x_offset = target.x - source.x;964 int screen_y_offset = target.y - source.y;965 // And how many pixels into the image itself does that966 // correlate to?967 int img_x_offset = (int)(screen_x_offset * imageXScaling + 0.5);968 int img_y_offset = (int)(screen_y_offset * imageYScaling + 0.5);969 // Now calculate the other corner of the image that we need970 // by scaling the 'target' rectangle's dimensions.971 int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling + 0.5);972 int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling + 0.5);973 974 if (Main.isDebugEnabled()) {975 Main.debug("drawing image into target rect: " + target);976 }977 g.drawImage(sourceImg,978 target.x, target.y,979 target.x + target.width, target.y + target.height,980 img_x_offset, img_y_offset,981 img_x_end, img_y_end,982 this);983 if (PROP_FADE_AMOUNT.get() != 0) {984 // dimm by painting opaque rect...985 g.setColor(getFadeColorWithAlpha());986 g.fillRect(target.x, target.y,987 target.width, target.height);988 }989 }990 991 // This function is called for several zoom levels, not just992 // the current one. It should not trigger any tiles to be993 // downloaded. It should also avoid polluting the tile cache994 // with any tiles since these tiles are not mandatory.995 //996 // The "border" tile tells us the boundaries of where we may997 // draw. It will not be from the zoom level that is being998 // drawn currently. If drawing the displayZoomLevel,999 // border is null and we draw the entire tile set.1000 private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {1001 if (zoom <= 0) return Collections.emptyList();1002 Rectangle borderRect = null;1003 if (border != null) {1004 borderRect = tileToRect(border);1005 }1006 List<Tile> missedTiles = new LinkedList<>();1007 // The callers of this code *require* that we return any tiles1008 // that we do not draw in missedTiles. ts.allExistingTiles() by1009 // default will only return already-existing tiles. However, we1010 // need to return *all* tiles to the callers, so force creation1011 // here.1012 //boolean forceTileCreation = true;1013 for (Tile tile : ts.allTilesCreate()) {1014 Image img = getLoadedTileImage(tile);1015 if (img == null || tile.hasError()) {1016 if (Main.isDebugEnabled()) {1017 Main.debug("missed tile: " + tile);1018 }1019 missedTiles.add(tile);1020 continue;1021 }1022 Rectangle sourceRect = tileToRect(tile);1023 if (borderRect != null && !sourceRect.intersects(borderRect)) {1024 continue;1025 }1026 drawImageInside(g, img, sourceRect, borderRect);1027 }1028 return missedTiles;1029 }1030 1031 private void myDrawString(Graphics g, String text, int x, int y) {1032 Color oldColor = g.getColor();1033 g.setColor(Color.black);1034 g.drawString(text,x+1,y+1);1035 g.setColor(oldColor);1036 g.drawString(text,x,y);1037 }1038 1039 private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {1040 int fontHeight = g.getFontMetrics().getHeight();1041 if (tile == null)1042 return;1043 Point p = pixelPos(t);1044 int texty = p.y + 2 + fontHeight;1045 1046 /*if (PROP_DRAW_DEBUG.get()) {1047 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);1048 texty += 1 + fontHeight;1049 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {1050 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);1051 texty += 1 + fontHeight;1052 }1053 }*/1054 1055 if (tile == showMetadataTile) {1056 String md = tile.toString();1057 if (md != null) {1058 myDrawString(g, md, p.x + 2, texty);1059 texty += 1 + fontHeight;1060 }1061 Map<String, String> meta = tile.getMetadata();1062 if (meta != null) {1063 for (Map.Entry<String, String> entry : meta.entrySet()) {1064 myDrawString(g, entry.getKey() + ": " + entry.getValue(), p.x + 2, texty);1065 texty += 1 + fontHeight;1066 }1067 }1068 }1069 1070 /*String tileStatus = tile.getStatus();1071 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {1072 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);1073 texty += 1 + fontHeight;1074 }*/1075 1076 if (tile.hasError() && showErrors) {1077 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);1078 texty += 1 + fontHeight;1079 }1080 1081 /*int xCursor = -1;1082 int yCursor = -1;1083 if (PROP_DRAW_DEBUG.get()) {1084 if (yCursor < t.getYtile()) {1085 if (t.getYtile() % 32 == 31) {1086 g.fillRect(0, p.y - 1, mv.getWidth(), 3);1087 } else {1088 g.drawLine(0, p.y, mv.getWidth(), p.y);1089 }1090 yCursor = t.getYtile();1091 }1092 // This draws the vertical lines for the entire1093 // column. Only draw them for the top tile in1094 // the column.1095 if (xCursor < t.getXtile()) {1096 if (t.getXtile() % 32 == 0) {1097 // level 7 tile boundary1098 g.fillRect(p.x - 1, 0, 3, mv.getHeight());1099 } else {1100 g.drawLine(p.x, 0, p.x, mv.getHeight());1101 }1102 xCursor = t.getXtile();1103 }1104 }*/1105 }1106 1107 private Point pixelPos(LatLon ll) {1108 return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy()));1109 }1110 1111 private Point pixelPos(Tile t) {1112 double lon = tileSource.tileXToLon(t.getXtile(), t.getZoom());1113 LatLon tmpLL = new LatLon(tileSource.tileYToLat(t.getYtile(), t.getZoom()), lon);1114 return pixelPos(tmpLL);1115 }1116 1117 private LatLon getShiftedLatLon(EastNorth en) {1118 return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy()));1119 }1120 1121 private Coordinate getShiftedCoord(EastNorth en) {1122 LatLon ll = getShiftedLatLon(en);1123 return new Coordinate(ll.lat(),ll.lon());1124 }1125 1126 private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0);1127 private final class TileSet {1128 private int x0, x1, y0, y1;1129 private int zoom;1130 private int tileMax = -1;1131 1132 /**1133 * Create a TileSet by EastNorth bbox taking a layer shift in account1134 */1135 private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {1136 this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);1137 }1138 1139 /**1140 * Create a TileSet by known LatLon bbox without layer shift correction1141 */1142 private TileSet(LatLon topLeft, LatLon botRight, int zoom) {1143 this.zoom = zoom;1144 if (zoom == 0)1145 return;1146 1147 x0 = (int)tileSource.lonToTileX(topLeft.lon(), zoom);1148 y0 = (int)tileSource.latToTileY(topLeft.lat(), zoom);1149 x1 = (int)tileSource.lonToTileX(botRight.lon(), zoom);1150 y1 = (int)tileSource.latToTileY(botRight.lat(), zoom);1151 if (x0 > x1) {1152 int tmp = x0;1153 x0 = x1;1154 x1 = tmp;1155 }1156 if (y0 > y1) {1157 int tmp = y0;1158 y0 = y1;1159 y1 = tmp;1160 }1161 tileMax = (int)Math.pow(2.0, zoom);1162 if (x0 < 0) {1163 x0 = 0;1164 }1165 if (y0 < 0) {1166 y0 = 0;1167 }1168 if (x1 > tileMax) {1169 x1 = tileMax;1170 }1171 if (y1 > tileMax) {1172 y1 = tileMax;1173 }1174 }1175 1176 private boolean tooSmall() {1177 return this.tilesSpanned() < 2.1;1178 }1179 1180 private boolean tooLarge() {1181 return this.tilesSpanned() > 10;1182 }1183 1184 private boolean insane() {1185 return this.tilesSpanned() > 100;1186 }1187 1188 private double tilesSpanned() {1189 return Math.sqrt(1.0 * this.size());1190 }1191 1192 private int size() {1193 int x_span = x1 - x0 + 1;1194 int y_span = y1 - y0 + 1;1195 return x_span * y_span;1196 }1197 1198 /*1199 * Get all tiles represented by this TileSet that are1200 * already in the tileCache.1201 */1202 private List<Tile> allExistingTiles() {1203 return this.__allTiles(false);1204 }1205 1206 private List<Tile> allTilesCreate() {1207 return this.__allTiles(true);1208 }1209 1210 private List<Tile> __allTiles(boolean create) {1211 // Tileset is either empty or too large1212 if (zoom == 0 || this.insane())1213 return Collections.emptyList();1214 List<Tile> ret = new ArrayList<>();1215 for (int x = x0; x <= x1; x++) {1216 for (int y = y0; y <= y1; y++) {1217 Tile t;1218 if (create) {1219 t = getOrCreateTile(x % tileMax, y % tileMax, zoom);1220 } else {1221 t = getTile(x % tileMax, y % tileMax, zoom);1222 }1223 if (t != null) {1224 ret.add(t);1225 }1226 }1227 }1228 return ret;1229 }1230 1231 private List<Tile> allLoadedTiles() {1232 List<Tile> ret = new ArrayList<>();1233 for (Tile t : this.allExistingTiles()) {1234 if (t.isLoaded())1235 ret.add(t);1236 }1237 return ret;1238 }1239 1240 private Comparator<Tile> getTileDistanceComparator() {1241 final int centerX = (int) Math.ceil((x0 + x1) / 2);1242 final int centerY = (int) Math.ceil((y0 + y1) / 2);1243 return new Comparator<Tile>() {1244 private int getDistance(Tile t) {1245 return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY);1246 }1247 @Override1248 public int compare(Tile o1, Tile o2) {1249 int distance1 = getDistance(o1);1250 int distance2 = getDistance(o2);1251 return Integer.compare(distance1, distance2);1252 }1253 };1254 }1255 1256 private void loadAllTiles(boolean force) {1257 if (!autoLoad && !force)1258 return;1259 List<Tile> allTiles = allTilesCreate();1260 Collections.sort(allTiles, getTileDistanceComparator());1261 for (Tile t : allTiles) { //, getTileDistanceComparator())) {1262 loadTile(t, false);1263 }1264 }1265 1266 private void loadAllErrorTiles(boolean force) {1267 if (!autoLoad && !force)1268 return;1269 for (Tile t : this.allTilesCreate()) {1270 if (t.hasError()) {1271 loadTile(t, true);1272 }1273 }1274 }1275 }1276 1277 1278 private static class TileSetInfo {1279 public boolean hasVisibleTiles = false;1280 public boolean hasOverzoomedTiles = false;1281 public boolean hasLoadingTiles = false;1282 }1283 1284 private static TileSetInfo getTileSetInfo(TileSet ts) {1285 List<Tile> allTiles = ts.allExistingTiles();1286 TileSetInfo result = new TileSetInfo();1287 result.hasLoadingTiles = allTiles.size() < ts.size();1288 for (Tile t : allTiles) {1289 if (t.isLoaded()) {1290 if (!t.hasError()) {1291 result.hasVisibleTiles = true;1292 }1293 if ("no-tile".equals(t.getValue("tile-info"))) {1294 result.hasOverzoomedTiles = true;1295 }1296 } else {1297 result.hasLoadingTiles = true;1298 }1299 }1300 return result;1301 }1302 1303 private class DeepTileSet {1304 private final EastNorth topLeft, botRight;1305 private final int minZoom, maxZoom;1306 private final TileSet[] tileSets;1307 private final TileSetInfo[] tileSetInfos;1308 public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {1309 this.topLeft = topLeft;1310 this.botRight = botRight;1311 this.minZoom = minZoom;1312 this.maxZoom = maxZoom;1313 this.tileSets = new TileSet[maxZoom - minZoom + 1];1314 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];1315 }1316 public TileSet getTileSet(int zoom) {1317 if (zoom < minZoom)1318 return nullTileSet;1319 synchronized (tileSets) {1320 TileSet ts = tileSets[zoom-minZoom];1321 if (ts == null) {1322 ts = new TileSet(topLeft, botRight, zoom);1323 tileSets[zoom-minZoom] = ts;1324 }1325 return ts;1326 }1327 }1328 1329 public TileSetInfo getTileSetInfo(int zoom) {1330 if (zoom < minZoom)1331 return new TileSetInfo();1332 synchronized (tileSetInfos) {1333 TileSetInfo tsi = tileSetInfos[zoom-minZoom];1334 if (tsi == null) {1335 tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));1336 tileSetInfos[zoom-minZoom] = tsi;1337 }1338 return tsi;1339 }1340 }1341 }1342 1343 @Override1344 public void paint(Graphics2D g, MapView mv, Bounds bounds) {1345 EastNorth topLeft = mv.getEastNorth(0, 0);1346 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());1347 1348 if (botRight.east() == 0 || botRight.north() == 0) {1349 /*Main.debug("still initializing??");*/1350 // probably still initializing1351 return;1352 }1353 1354 needRedraw = false;1355 1356 int zoom = currentZoomLevel;1357 if (autoZoom) {1358 double pixelScaling = getScaleFactor(zoom);1359 if (pixelScaling > 3 || pixelScaling < 0.7) {1360 zoom = getBestZoom();1361 }1362 }1363 1364 DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);1365 TileSet ts = dts.getTileSet(zoom);1366 1367 int displayZoomLevel = zoom;1368 1369 boolean noTilesAtZoom = false;1370 if (autoZoom && autoLoad) {1371 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)1372 TileSetInfo tsi = dts.getTileSetInfo(zoom);1373 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {1374 noTilesAtZoom = true;1375 }1376 // Find highest zoom level with at least one visible tile1377 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {1378 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {1379 displayZoomLevel = tmpZoom;1380 break;1381 }1382 }1383 // Do binary search between currentZoomLevel and displayZoomLevel1384 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){1385 zoom = (zoom + displayZoomLevel)/2;1386 tsi = dts.getTileSetInfo(zoom);1387 }1388 1389 setZoomLevel(zoom);1390 1391 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level1392 // to make sure there're really no more zoom levels1393 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {1394 zoom++;1395 tsi = dts.getTileSetInfo(zoom);1396 }1397 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,1398 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.1399 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {1400 zoom--;1401 tsi = dts.getTileSetInfo(zoom);1402 }1403 ts = dts.getTileSet(zoom);1404 } else if (autoZoom) {1405 setZoomLevel(zoom);1406 }1407 1408 // Too many tiles... refuse to download1409 if (!ts.tooLarge()) {1410 //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());1411 ts.loadAllTiles(false);1412 }1413 1414 if (displayZoomLevel != zoom) {1415 ts = dts.getTileSet(displayZoomLevel);1416 }1417 1418 g.setColor(Color.DARK_GRAY);1419 1420 List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);1421 int[] otherZooms = { -1, 1, -2, 2, -3, -4, -5};1422 for (int zoomOffset : otherZooms) {1423 if (!autoZoom) {1424 break;1425 }1426 int newzoom = displayZoomLevel + zoomOffset;1427 if (newzoom < MIN_ZOOM) {1428 continue;1429 }1430 if (missedTiles.isEmpty()) {1431 break;1432 }1433 List<Tile> newlyMissedTiles = new LinkedList<>();1434 for (Tile missed : missedTiles) {1435 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {1436 // Don't try to paint from higher zoom levels when tile is overzoomed1437 newlyMissedTiles.add(missed);1438 continue;1439 }1440 Tile t2 = tempCornerTile(missed);1441 LatLon topLeft2 = tileLatLon(missed);1442 LatLon botRight2 = tileLatLon(t2);1443 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);1444 // Instantiating large TileSets is expensive. If there1445 // are no loaded tiles, don't bother even trying.1446 if (ts2.allLoadedTiles().isEmpty()) {1447 newlyMissedTiles.add(missed);1448 continue;1449 }1450 if (ts2.tooLarge()) {1451 continue;1452 }1453 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));1454 }1455 missedTiles = newlyMissedTiles;1456 }1457 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {1458 Main.debug("still missed "+missedTiles.size()+" in the end");1459 }1460 g.setColor(Color.red);1461 g.setFont(InfoFont);1462 1463 // The current zoom tileset should have all of its tiles1464 // due to the loadAllTiles(), unless it to tooLarge()1465 for (Tile t : ts.allExistingTiles()) {1466 this.paintTileText(ts, t, g, mv, displayZoomLevel, t);1467 }1468 1469 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight), displayZoomLevel, this);1470 1471 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);1472 g.setColor(Color.lightGray);1473 if (!autoZoom) {1474 if (ts.insane()) {1475 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);1476 } else if (ts.tooLarge()) {1477 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);1478 } else if (ts.tooSmall()) {1479 myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);1480 }1481 }1482 if (noTilesAtZoom) {1483 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);1484 }1485 if (Main.isDebugEnabled()) {1486 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);1487 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);1488 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);1489 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);1490 if(tileLoader instanceof TMSCachedTileLoader) {1491 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader;1492 int offset = 185;1493 for(String part: cachedTileLoader.getStats().split("\n")) {1494 myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15);1495 }1496 1497 }1498 }1499 }1500 1501 /**1502 * This isn't very efficient, but it is only used when the1503 * user right-clicks on the map.1504 */1505 private Tile getTileForPixelpos(int px, int py) {1506 if (Main.isDebugEnabled()) {1507 Main.debug("getTileForPixelpos("+px+", "+py+")");1508 }1509 MapView mv = Main.map.mapView;1510 Point clicked = new Point(px, py);1511 EastNorth topLeft = mv.getEastNorth(0, 0);1512 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());1513 int z = currentZoomLevel;1514 TileSet ts = new TileSet(topLeft, botRight, z);1515 1516 if (!ts.tooLarge()) {1517 ts.loadAllTiles(false); // make sure there are tile objects for all tiles1518 }1519 Tile clickedTile = null;1520 for (Tile t1 : ts.allExistingTiles()) {1521 Tile t2 = tempCornerTile(t1);1522 Rectangle r = new Rectangle(pixelPos(t1));1523 r.add(pixelPos(t2));1524 if (Main.isDebugEnabled()) {1525 Main.debug("r: " + r + " clicked: " + clicked);1526 }1527 if (!r.contains(clicked)) {1528 continue;1529 }1530 clickedTile = t1;1531 break;1532 }1533 if (clickedTile == null)1534 return null;1535 /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +1536 " currentZoomLevel: " + currentZoomLevel);*/1537 return clickedTile;1538 }1539 1540 @Override1541 public Action[] getMenuEntries() {1542 return new Action[] {1543 LayerListDialog.getInstance().createShowHideLayerAction(),1544 LayerListDialog.getInstance().createDeleteLayerAction(),1545 SeparatorLayerAction.INSTANCE,1546 // color,1547 new OffsetAction(),1548 new RenameLayerAction(this.getAssociatedFile(), this),1549 SeparatorLayerAction.INSTANCE,1550 new LayerListPopup.InfoAction(this) };1551 }1552 1553 @Override1554 public String getToolTipText() {1555 return tr("TMS layer ({0}), downloading in zoom {1}", getName(), currentZoomLevel);1556 }1557 1558 @Override1559 public void visitBoundingBox(BoundingXYVisitor v) {1560 }1561 1562 @Override1563 public boolean isChanged() {1564 return needRedraw;1565 }1566 105 1567 106 @Override 1568 107 public final boolean isProjectionSupported(Projection proj) { -
src/org/openstreetmap/josm/gui/layer/WMSLayer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/WMSLayer.java b/src/org/openstreetmap/josm/gui/layer/WMSLayer.java index 3bc2a12..443a07b 100644
a b package org.openstreetmap.josm.gui.layer; 3 3 4 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 5 6 import java.awt.Component;7 import java.awt.Graphics;8 import java.awt.Graphics2D;9 import java.awt.Image;10 import java.awt.Point;11 6 import java.awt.event.ActionEvent; 12 import java.awt.event.MouseAdapter;13 import java.awt.event.MouseEvent;14 import java.awt.image.BufferedImage;15 import java.awt.image.ImageObserver;16 import java.io.Externalizable;17 import java.io.File;18 7 import java.io.IOException; 19 import java.io.InvalidClassException;20 import java.io.ObjectInput;21 import java.io.ObjectOutput;22 8 import java.util.ArrayList; 23 import java.util.Collections; 24 import java.util.HashSet; 25 import java.util.Iterator; 9 import java.util.Arrays; 26 10 import java.util.List; 27 import java.util.Locale; 28 import java.util.Set; 29 import java.util.concurrent.locks.Condition; 30 import java.util.concurrent.locks.Lock; 31 import java.util.concurrent.locks.ReentrantLock; 11 import java.util.Map; 32 12 33 13 import javax.swing.AbstractAction; 34 14 import javax.swing.Action; 35 import javax.swing.JCheckBoxMenuItem;36 import javax.swing.JMenuItem;37 import javax.swing.JOptionPane;38 15 39 import org.openstreetmap.gui.jmapviewer.AttributionSupport; 40 import org.openstreetmap.josm.Main; 41 import org.openstreetmap.josm.actions.SaveActionBase; 42 import org.openstreetmap.josm.data.Bounds; 43 import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 44 import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 45 import org.openstreetmap.josm.data.ProjectionBounds; 46 import org.openstreetmap.josm.data.coor.EastNorth; 47 import org.openstreetmap.josm.data.coor.LatLon; 48 import org.openstreetmap.josm.data.imagery.GeorefImage; 49 import org.openstreetmap.josm.data.imagery.GeorefImage.State; 16 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 17 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 18 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 19 import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory; 50 20 import org.openstreetmap.josm.data.imagery.ImageryInfo; 51 21 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 52 22 import org.openstreetmap.josm.data.imagery.ImageryLayerInfo; 53 import org.openstreetmap.josm.data.imagery. WmsCache;54 import org.openstreetmap.josm.data.imagery. types.ObjectFactory;55 import org.openstreetmap.josm.data. osm.visitor.BoundingXYVisitor;23 import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource; 24 import org.openstreetmap.josm.data.imagery.TileLoaderFactory; 25 import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader; 56 26 import org.openstreetmap.josm.data.preferences.BooleanProperty; 57 27 import org.openstreetmap.josm.data.preferences.IntegerProperty; 58 import org.openstreetmap.josm.data.projection.Projection;59 import org.openstreetmap.josm.gui.MapView;60 import org.openstreetmap.josm.gui.MapView.LayerChangeListener;61 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;62 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;63 import org.openstreetmap.josm.gui.progress.ProgressMonitor;64 import org.openstreetmap.josm.io.WMSLayerImporter;65 import org.openstreetmap.josm.io.imagery.HTMLGrabber;66 import org.openstreetmap.josm.io.imagery.WMSException;67 import org.openstreetmap.josm.io.imagery.WMSGrabber;68 import org.openstreetmap.josm.io.imagery.WMSRequest;69 import org.openstreetmap.josm.tools.ImageProvider;70 import org.openstreetmap.josm.tools.Utils;71 28 72 29 /** 73 30 * This is a layer that grabs the current screen from an WMS server. The data 74 31 * fetched this way is tiled and managed to the disc to reduce server load. 32 * 75 33 */ 76 public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceChangedListener, Externalizable { 77 78 public static class PrecacheTask { 79 private final ProgressMonitor progressMonitor; 80 private volatile int totalCount; 81 private volatile int processedCount; 82 private volatile boolean isCancelled; 83 84 public PrecacheTask(ProgressMonitor progressMonitor) { 85 this.progressMonitor = progressMonitor; 86 } 87 88 public boolean isFinished() { 89 return totalCount == processedCount; 90 } 91 92 public int getTotalCount() { 93 return totalCount; 94 } 95 96 public void cancel() { 97 isCancelled = true; 98 } 99 } 100 101 // Fake reference to keep build scripts from removing ObjectFactory class. This class is not used directly but it's necessary for jaxb to work 102 @SuppressWarnings("unused") 103 private static final ObjectFactory OBJECT_FACTORY = null; 104 105 // these values correspond to the zoom levels used throughout OSM and are in meters/pixel from zoom level 0 to 18. 106 // taken from http://wiki.openstreetmap.org/wiki/Zoom_levels 107 private static final Double[] snapLevels = { 156412.0, 78206.0, 39103.0, 19551.0, 9776.0, 4888.0, 108 2444.0, 1222.0, 610.984, 305.492, 152.746, 76.373, 38.187, 19.093, 9.547, 4.773, 2.387, 1.193, 0.596 }; 109 110 public static final BooleanProperty PROP_ALPHA_CHANNEL = new BooleanProperty("imagery.wms.alpha_channel", true); 111 public static final IntegerProperty PROP_SIMULTANEOUS_CONNECTIONS = new IntegerProperty("imagery.wms.simultaneousConnections", 3); 112 public static final BooleanProperty PROP_OVERLAP = new BooleanProperty("imagery.wms.overlap", false); 113 public static final IntegerProperty PROP_OVERLAP_EAST = new IntegerProperty("imagery.wms.overlapEast", 14); 114 public static final IntegerProperty PROP_OVERLAP_NORTH = new IntegerProperty("imagery.wms.overlapNorth", 4); 115 public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty("imagery.wms.imageSize", 500); 34 public class WMSLayer extends AbstractTileSourceLayer { 35 /** default tile size for WMS Layer */ 36 public static final IntegerProperty PROP_IMAGE_SIZE = new IntegerProperty("imagery.wms.imageSize", 512); 37 /** should WMS layer autozoom in default mode */ 116 38 public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty("imagery.wms.default_autozoom", true); 117 39 118 public int messageNum = 5; //limit for messages per layer119 protected double resolution;120 protected String resolutionText;121 protected int imageSize;122 protected int dax = 10;123 protected int day = 10;124 protected int daStep = 5;125 protected int minZoom = 3;126 127 protected GeorefImage[][] images;128 protected static final int serializeFormatVersion = 5;129 protected boolean autoDownloadEnabled = true;130 protected boolean autoResolutionEnabled = PROP_DEFAULT_AUTOZOOM.get();131 protected boolean settingsChanged;132 public transient WmsCache cache;133 private transient AttributionSupport attribution = new AttributionSupport();134 135 // Image index boundary for current view136 private volatile int bminx;137 private volatile int bminy;138 private volatile int bmaxx;139 private volatile int bmaxy;140 private volatile int leftEdge;141 private volatile int bottomEdge;142 143 // Request queue144 private final transient List<WMSRequest> requestQueue = new ArrayList<>();145 private final transient List<WMSRequest> finishedRequests = new ArrayList<>();146 /**147 * List of request currently being processed by download threads148 */149 private final transient List<WMSRequest> processingRequests = new ArrayList<>();150 private final transient Lock requestQueueLock = new ReentrantLock();151 private final transient Condition queueEmpty = requestQueueLock.newCondition();152 private final transient List<WMSGrabber> grabbers = new ArrayList<>();153 private final transient List<Thread> grabberThreads = new ArrayList<>();154 private boolean canceled;155 156 /** set to true if this layer uses an invalid base url */157 private boolean usesInvalidUrl = false;158 /** set to true if the user confirmed to use an potentially invalid WMS base url */159 private boolean isInvalidUrlConfirmed = false;160 161 /**162 * Constructs a new {@code WMSLayer}.163 */164 public WMSLayer() {165 this(new ImageryInfo(tr("Blank Layer")));166 }167 168 40 /** 169 41 * Constructs a new {@code WMSLayer}. 42 * @param info ImageryInfo description of the layer 170 43 */ 171 44 public WMSLayer(ImageryInfo info) { 172 45 super(info); 173 imageSize = PROP_IMAGE_SIZE.get();174 setBackgroundLayer(true); /* set global background variable */175 initializeImages();176 177 attribution.initialize(this.info);178 179 Main.pref.addPreferenceChangeListener(this);180 }181 182 @Override183 public void hookUpMapView() {184 if (info.getUrl() != null) {185 startGrabberThreads();186 187 for (WMSLayer layer: Main.map.mapView.getLayersOfType(WMSLayer.class)) {188 if (layer.getInfo().getUrl().equals(info.getUrl())) {189 cache = layer.cache;190 break;191 }192 }193 if (cache == null) {194 cache = new WmsCache(info.getUrl(), imageSize);195 cache.loadIndex();196 }197 }198 199 // if automatic resolution is enabled, ensure that the first zoom level200 // is already snapped. Otherwise it may load tiles that will never get201 // used again when zooming.202 updateResolutionSetting(this, autoResolutionEnabled);203 204 final MouseAdapter adapter = new MouseAdapter() {205 @Override206 public void mouseClicked(MouseEvent e) {207 if (!isVisible()) return;208 if (e.getButton() == MouseEvent.BUTTON1) {209 attribution.handleAttribution(e.getPoint(), true);210 }211 }212 };213 Main.map.mapView.addMouseListener(adapter);214 215 MapView.addLayerChangeListener(new LayerChangeListener() {216 @Override217 public void activeLayerChange(Layer oldLayer, Layer newLayer) {218 //219 }220 221 @Override222 public void layerAdded(Layer newLayer) {223 //224 }225 226 @Override227 public void layerRemoved(Layer oldLayer) {228 if (oldLayer == WMSLayer.this) {229 Main.map.mapView.removeMouseListener(adapter);230 MapView.removeLayerChangeListener(this);231 }232 }233 });234 }235 236 public void doSetName(String name) {237 setName(name);238 info.setName(name);239 }240 241 public boolean hasAutoDownload(){242 return autoDownloadEnabled;243 }244 245 public void setAutoDownload(boolean val) {246 autoDownloadEnabled = val;247 }248 249 public boolean isAutoResolution() {250 return autoResolutionEnabled;251 }252 253 public void setAutoResolution(boolean val) {254 autoResolutionEnabled = val;255 }256 257 public void downloadAreaToCache(PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {258 Set<Point> requestedTiles = new HashSet<>();259 for (LatLon point: points) {260 EastNorth minEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() - bufferY, point.lon() - bufferX));261 EastNorth maxEn = Main.getProjection().latlon2eastNorth(new LatLon(point.lat() + bufferY, point.lon() + bufferX));262 int minX = getImageXIndex(minEn.east());263 int maxX = getImageXIndex(maxEn.east());264 int minY = getImageYIndex(minEn.north());265 int maxY = getImageYIndex(maxEn.north());266 267 for (int x=minX; x<=maxX; x++) {268 for (int y=minY; y<=maxY; y++) {269 requestedTiles.add(new Point(x, y));270 }271 }272 }273 274 for (Point p: requestedTiles) {275 addRequest(new WMSRequest(p.x, p.y, info.getPixelPerDegree(), true, false, precacheTask));276 }277 278 precacheTask.progressMonitor.setTicksCount(precacheTask.getTotalCount());279 precacheTask.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", 0, precacheTask.totalCount));280 }281 282 @Override283 public void destroy() {284 super.destroy();285 cancelGrabberThreads(false);286 Main.pref.removePreferenceChangeListener(this);287 if (cache != null) {288 cache.saveIndex();289 }290 }291 292 public final void initializeImages() {293 GeorefImage[][] old = images;294 images = new GeorefImage[dax][day];295 if (old != null) {296 for (GeorefImage[] row : old) {297 for (GeorefImage image : row) {298 images[modulo(image.getXIndex(), dax)][modulo(image.getYIndex(), day)] = image;299 }300 }301 }302 for(int x = 0; x<dax; ++x) {303 for(int y = 0; y<day; ++y) {304 if (images[x][y] == null) {305 images[x][y]= new GeorefImage(this);306 }307 }308 }309 }310 311 @Override public ImageryInfo getInfo() {312 return info;313 }314 315 @Override public String getToolTipText() {316 if(autoDownloadEnabled)317 return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolutionText);318 else319 return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolutionText);320 }321 322 private int modulo(int a, int b) {323 return a % b >= 0 ? a%b : a%b+b;324 }325 326 private boolean zoomIsTooBig() {327 //don't download when it's too outzoomed328 return info.getPixelPerDegree() / getPPD() > minZoom;329 46 } 330 47 331 48 @Override 332 public void paint(Graphics2D g, final MapView mv, Bounds b) { 333 if(info.getUrl() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return; 334 335 if (autoResolutionEnabled && !Utils.equalsEpsilon(getBestZoom(), mv.getDist100Pixel())) { 336 changeResolution(this, true); 337 } 338 339 settingsChanged = false; 340 341 ProjectionBounds bounds = mv.getProjectionBounds(); 342 bminx= getImageXIndex(bounds.minEast); 343 bminy= getImageYIndex(bounds.minNorth); 344 bmaxx= getImageXIndex(bounds.maxEast); 345 bmaxy= getImageYIndex(bounds.maxNorth); 346 347 leftEdge = (int)(bounds.minEast * getPPD()); 348 bottomEdge = (int)(bounds.minNorth * getPPD()); 349 350 if (zoomIsTooBig()) { 351 for(int x = 0; x<images.length; ++x) { 352 for(int y = 0; y<images[0].length; ++y) { 353 GeorefImage image = images[x][y]; 354 image.paint(g, mv, image.getXIndex(), image.getYIndex(), leftEdge, bottomEdge); 355 } 356 } 357 } else { 358 downloadAndPaintVisible(g, mv, false); 49 public Action[] getMenuEntries() { 50 List<Action> ret = new ArrayList<>(); 51 ret.addAll(Arrays.asList(super.getMenuEntries())); 52 ret.add(SeparatorLayerAction.INSTANCE); 53 ret.add(new LayerSaveAction(this)); 54 ret.add(new LayerSaveAsAction(this)); 55 ret.add(new BookmarkWmsAction()); 56 return ret.toArray(new Action[]{}); 359 57 } 360 58 361 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), null, null, 0, this);362 }363 59 364 60 @Override 365 public void setOffset(double dx, double dy) { 366 super.setOffset(dx, dy); 367 settingsChanged = true; 368 } 369 370 public int getImageXIndex(double coord) { 371 return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize); 372 } 373 374 public int getImageYIndex(double coord) { 375 return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize); 376 } 377 378 public int getImageX(int imageIndex) { 379 return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD()); 380 } 381 382 public int getImageY(int imageIndex) { 383 return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD()); 384 } 385 386 public int getImageWidth(int xIndex) { 387 return getImageX(xIndex + 1) - getImageX(xIndex); 388 } 389 390 public int getImageHeight(int yIndex) { 391 return getImageY(yIndex + 1) - getImageY(yIndex); 392 } 393 394 /** 395 * 396 * @return Size of image in original zoom 397 */ 398 public int getBaseImageWidth() { 399 int overlap = PROP_OVERLAP.get() ? PROP_OVERLAP_EAST.get() * imageSize / 100 : 0; 400 return imageSize + overlap; 401 } 402 403 /** 404 * 405 * @return Size of image in original zoom 406 */ 407 public int getBaseImageHeight() { 408 int overlap = PROP_OVERLAP.get() ? PROP_OVERLAP_NORTH.get() * imageSize / 100 : 0; 409 return imageSize + overlap; 410 } 411 412 public int getImageSize() { 413 return imageSize; 414 } 415 416 public boolean isOverlapEnabled() { 417 return WMSLayer.PROP_OVERLAP.get() && (WMSLayer.PROP_OVERLAP_EAST.get() > 0 || WMSLayer.PROP_OVERLAP_NORTH.get() > 0); 418 } 419 420 /** 421 * 422 * @return When overlapping is enabled, return visible part of tile. Otherwise return original image 423 */ 424 public BufferedImage normalizeImage(BufferedImage img) { 425 if (isOverlapEnabled()) { 426 BufferedImage copy = img; 427 img = new BufferedImage(imageSize, imageSize, copy.getType()); 428 img.createGraphics().drawImage(copy, 0, 0, imageSize, imageSize, 429 0, copy.getHeight() - imageSize, imageSize, copy.getHeight(), null); 430 } 431 return img; 432 } 433 434 /** 435 * 436 * @param xIndex 437 * @param yIndex 438 * @return Real EastNorth of given tile. dx/dy is not counted in 439 */ 440 public EastNorth getEastNorth(int xIndex, int yIndex) { 441 return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree()); 442 } 443 444 protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real){ 445 446 int newDax = dax; 447 int newDay = day; 448 449 if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) { 450 newDax = ((bmaxx - bminx) / daStep + 1) * daStep; 451 } 452 453 if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) { 454 newDay = ((bmaxy - bminy) / daStep + 1) * daStep; 455 } 456 457 if (newDax != dax || newDay != day) { 458 dax = newDax; 459 day = newDay; 460 initializeImages(); 461 } 462 463 for(int x = bminx; x<=bmaxx; ++x) { 464 for(int y = bminy; y<=bmaxy; ++y){ 465 images[modulo(x,dax)][modulo(y,day)].changePosition(x, y); 466 } 467 } 468 469 gatherFinishedRequests(); 470 Set<ProjectionBounds> areaToCache = new HashSet<>(); 471 472 for(int x = bminx; x<=bmaxx; ++x) { 473 for(int y = bminy; y<=bmaxy; ++y){ 474 GeorefImage img = images[modulo(x,dax)][modulo(y,day)]; 475 if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) { 476 addRequest(new WMSRequest(x, y, info.getPixelPerDegree(), real, true)); 477 areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1))); 478 } else if (img.getState() == State.PARTLY_IN_CACHE && autoDownloadEnabled) { 479 addRequest(new WMSRequest(x, y, info.getPixelPerDegree(), real, false)); 480 areaToCache.add(new ProjectionBounds(getEastNorth(x, y), getEastNorth(x + 1, y + 1))); 481 } 482 } 483 } 484 if (cache != null) { 485 cache.setAreaToCache(areaToCache); 486 } 487 } 488 489 @Override public void visitBoundingBox(BoundingXYVisitor v) { 490 for(int x = 0; x<dax; ++x) { 491 for(int y = 0; y<day; ++y) 492 if(images[x][y].getImage() != null){ 493 v.visit(images[x][y].getMin()); 494 v.visit(images[x][y].getMax()); 495 } 496 } 497 } 498 499 @Override public Action[] getMenuEntries() { 500 return new Action[]{ 501 LayerListDialog.getInstance().createActivateLayerAction(this), 502 LayerListDialog.getInstance().createShowHideLayerAction(), 503 LayerListDialog.getInstance().createDeleteLayerAction(), 504 SeparatorLayerAction.INSTANCE, 505 new OffsetAction(), 506 new LayerSaveAction(this), 507 new LayerSaveAsAction(this), 508 new BookmarkWmsAction(), 509 SeparatorLayerAction.INSTANCE, 510 new StartStopAction(), 511 new ToggleAlphaAction(), 512 new ToggleAutoResolutionAction(), 513 new ChangeResolutionAction(), 514 new ZoomToNativeResolution(), 515 new ReloadErrorTilesAction(), 516 new DownloadAction(), 517 SeparatorLayerAction.INSTANCE, 518 new LayerListPopup.InfoAction(this) 519 }; 61 public TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException { 62 if (info.getImageryType() == ImageryType.WMS && info.getUrl() != null) { 63 TemplatedWMSTileSource.checkUrl(info.getUrl()); 64 TemplatedWMSTileSource tileSource = new TemplatedWMSTileSource(info); 65 info.setAttribution(tileSource); 66 return tileSource; 520 67 } 521 522 public GeorefImage findImage(EastNorth eastNorth) {523 int xIndex = getImageXIndex(eastNorth.east());524 int yIndex = getImageYIndex(eastNorth.north());525 GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)];526 if (result.getXIndex() == xIndex && result.getYIndex() == yIndex)527 return result;528 else529 68 return null; 530 69 } 531 70 532 71 /** 533 *534 * @param request535 * @return -1 if request is no longer needed, otherwise priority of request (lower number <=> more important request)536 */537 private int getRequestPriority(WMSRequest request) {538 if (!Utils.equalsEpsilon(request.getPixelPerDegree(), info.getPixelPerDegree()))539 return -1;540 if (bminx > request.getXIndex()541 || bmaxx < request.getXIndex()542 || bminy > request.getYIndex()543 || bmaxy < request.getYIndex())544 return -1;545 546 MouseEvent lastMEvent = Main.map.mapView.lastMEvent;547 EastNorth cursorEastNorth = Main.map.mapView.getEastNorth(lastMEvent.getX(), lastMEvent.getY());548 int mouseX = getImageXIndex(cursorEastNorth.east());549 int mouseY = getImageYIndex(cursorEastNorth.north());550 int dx = request.getXIndex() - mouseX;551 int dy = request.getYIndex() - mouseY;552 553 return 1 + dx * dx + dy * dy;554 }555 556 private void sortRequests(boolean localOnly) {557 Iterator<WMSRequest> it = requestQueue.iterator();558 while (it.hasNext()) {559 WMSRequest item = it.next();560 561 if (item.getPrecacheTask() != null && item.getPrecacheTask().isCancelled) {562 it.remove();563 continue;564 }565 566 int priority = getRequestPriority(item);567 if (priority == -1 && item.isPrecacheOnly()) {568 priority = Integer.MAX_VALUE; // Still download, but prefer requests in current view569 }570 571 if (localOnly && !item.hasExactMatch()) {572 priority = Integer.MAX_VALUE; // Only interested in tiles that can be loaded from file immediately573 }574 575 if ( priority == -1576 || finishedRequests.contains(item)577 || processingRequests.contains(item)) {578 it.remove();579 } else {580 item.setPriority(priority);581 }582 }583 Collections.sort(requestQueue);584 }585 586 public WMSRequest getRequest(boolean localOnly) {587 requestQueueLock.lock();588 try {589 sortRequests(localOnly);590 while (!canceled && (requestQueue.isEmpty() || (localOnly && !requestQueue.get(0).hasExactMatch()))) {591 try {592 queueEmpty.await();593 sortRequests(localOnly);594 } catch (InterruptedException e) {595 Main.warn("InterruptedException in "+getClass().getSimpleName()+" during WMS request");596 }597 }598 599 if (canceled)600 return null;601 else {602 WMSRequest request = requestQueue.remove(0);603 processingRequests.add(request);604 return request;605 }606 607 } finally {608 requestQueueLock.unlock();609 }610 }611 612 public void finishRequest(WMSRequest request) {613 requestQueueLock.lock();614 try {615 PrecacheTask task = request.getPrecacheTask();616 if (task != null) {617 task.processedCount++;618 if (!task.progressMonitor.isCanceled()) {619 task.progressMonitor.worked(1);620 task.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", task.processedCount, task.totalCount));621 }622 }623 processingRequests.remove(request);624 if (request.getState() != null && !request.isPrecacheOnly()) {625 finishedRequests.add(request);626 if (Main.isDisplayingMapView()) {627 Main.map.mapView.repaint();628 }629 }630 } finally {631 requestQueueLock.unlock();632 }633 }634 635 public void addRequest(WMSRequest request) {636 requestQueueLock.lock();637 try {638 639 if (cache != null) {640 ProjectionBounds b = getBounds(request);641 // Checking for exact match is fast enough, no need to do it in separated thread642 request.setHasExactMatch(cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth));643 if (request.isPrecacheOnly() && request.hasExactMatch())644 return; // We already have this tile cached645 }646 647 if (!requestQueue.contains(request) && !finishedRequests.contains(request) && !processingRequests.contains(request)) {648 requestQueue.add(request);649 if (request.getPrecacheTask() != null) {650 request.getPrecacheTask().totalCount++;651 }652 queueEmpty.signalAll();653 }654 } finally {655 requestQueueLock.unlock();656 }657 }658 659 public boolean requestIsVisible(WMSRequest request) {660 return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();661 }662 663 private void gatherFinishedRequests() {664 requestQueueLock.lock();665 try {666 for (WMSRequest request: finishedRequests) {667 GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)];668 if (img.equalPosition(request.getXIndex(), request.getYIndex())) {669 WMSException we = request.getException();670 img.changeImage(request.getState(), request.getImage(), we != null ? we.getMessage() : null);671 }672 }673 } finally {674 requestQueueLock.unlock();675 finishedRequests.clear();676 }677 }678 679 public class DownloadAction extends AbstractAction {680 /**681 * Constructs a new {@code DownloadAction}.682 */683 public DownloadAction() {684 super(tr("Download visible tiles"));685 }686 @Override687 public void actionPerformed(ActionEvent ev) {688 if (zoomIsTooBig()) {689 JOptionPane.showMessageDialog(690 Main.parent,691 tr("The requested area is too big. Please zoom in a little, or change resolution"),692 tr("Error"),693 JOptionPane.ERROR_MESSAGE694 );695 } else {696 downloadAndPaintVisible(Main.map.mapView.getGraphics(), Main.map.mapView, true);697 }698 }699 }700 701 /**702 * Finds the most suitable resolution for the current zoom level, but prefers703 * higher resolutions. Snaps to values defined in snapLevels.704 * @return best zoom level705 */706 private static double getBestZoom() {707 // not sure why getDist100Pixel returns values corresponding to708 // the snapLevels, which are in meters per pixel. It works, though.709 double dist = Main.map.mapView.getDist100Pixel();710 for(int i = snapLevels.length-2; i >= 0; i--) {711 if(snapLevels[i+1]/3 + snapLevels[i]*2/3 > dist)712 return snapLevels[i+1];713 }714 return snapLevels[0];715 }716 717 /**718 * Updates the given layer’s resolution settings to the current zoom level. Does719 * not update existing tiles, only new ones will be subject to the new settings.720 *721 * @param layer722 * @param snap Set to true if the resolution should snap to certain values instead of723 * matching the current zoom level perfectly724 */725 private static void updateResolutionSetting(WMSLayer layer, boolean snap) {726 if(snap) {727 layer.resolution = getBestZoom();728 layer.resolutionText = MapView.getDistText(layer.resolution);729 } else {730 layer.resolution = Main.map.mapView.getDist100Pixel();731 layer.resolutionText = Main.map.mapView.getDist100PixelText();732 }733 layer.info.setPixelPerDegree(layer.getPPD());734 }735 736 /**737 * Updates the given layer’s resolution settings to the current zoom level and738 * updates existing tiles. If round is true, tiles will be updated gradually, if739 * false they will be removed instantly (and redrawn only after the new resolution740 * image has been loaded).741 * @param layer742 * @param snap Set to true if the resolution should snap to certain values instead of743 * matching the current zoom level perfectly744 */745 private static void changeResolution(WMSLayer layer, boolean snap) {746 updateResolutionSetting(layer, snap);747 748 layer.settingsChanged = true;749 750 // Don’t move tiles off screen when the resolution is rounded. This751 // prevents some flickering when zooming with auto-resolution enabled752 // and instead gradually updates each tile.753 if(!snap) {754 for(int x = 0; x<layer.dax; ++x) {755 for(int y = 0; y<layer.day; ++y) {756 layer.images[x][y].changePosition(-1, -1);757 }758 }759 }760 }761 762 public static class ChangeResolutionAction extends AbstractAction implements LayerAction {763 764 /**765 * Constructs a new {@code ChangeResolutionAction}766 */767 public ChangeResolutionAction() {768 super(tr("Change resolution"));769 }770 771 @Override772 public void actionPerformed(ActionEvent ev) {773 List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();774 for (Layer l: layers) {775 changeResolution((WMSLayer) l, false);776 }777 Main.map.mapView.repaint();778 }779 780 @Override781 public boolean supportLayers(List<Layer> layers) {782 for (Layer l: layers) {783 if (!(l instanceof WMSLayer))784 return false;785 }786 return true;787 }788 789 @Override790 public Component createMenuComponent() {791 return new JMenuItem(this);792 }793 }794 795 public class ReloadErrorTilesAction extends AbstractAction {796 /**797 * Constructs a new {@code ReloadErrorTilesAction}.798 */799 public ReloadErrorTilesAction() {800 super(tr("Reload erroneous tiles"));801 }802 @Override803 public void actionPerformed(ActionEvent ev) {804 // Delete small files, because they're probably blank tiles.805 // See #2307806 cache.cleanSmallFiles(4096);807 808 for (int x = 0; x < dax; ++x) {809 for (int y = 0; y < day; ++y) {810 GeorefImage img = images[modulo(x,dax)][modulo(y,day)];811 if(img.getState() == State.FAILED){812 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true, false));813 }814 }815 }816 }817 }818 819 public class ToggleAlphaAction extends AbstractAction implements LayerAction {820 /**821 * Constructs a new {@code ToggleAlphaAction}.822 */823 public ToggleAlphaAction() {824 super(tr("Alpha channel"));825 }826 @Override827 public void actionPerformed(ActionEvent ev) {828 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();829 boolean alphaChannel = checkbox.isSelected();830 PROP_ALPHA_CHANNEL.put(alphaChannel);831 Main.info("WMS Alpha channel changed to "+alphaChannel);832 833 // clear all resized cached instances and repaint the layer834 for (int x = 0; x < dax; ++x) {835 for (int y = 0; y < day; ++y) {836 GeorefImage img = images[modulo(x, dax)][modulo(y, day)];837 img.flushResizedCachedInstance();838 BufferedImage bi = img.getImage();839 // Completely erases images for which transparency has been forced,840 // or images that should be forced now, as they need to be recreated841 if (ImageProvider.isTransparencyForced(bi) || ImageProvider.hasTransparentColor(bi)) {842 img.resetImage();843 }844 }845 }846 Main.map.mapView.repaint();847 }848 849 @Override850 public Component createMenuComponent() {851 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);852 item.setSelected(PROP_ALPHA_CHANNEL.get());853 return item;854 }855 856 @Override857 public boolean supportLayers(List<Layer> layers) {858 return layers.size() == 1 && layers.get(0) instanceof WMSLayer;859 }860 }861 862 public class ToggleAutoResolutionAction extends AbstractAction implements LayerAction {863 864 /**865 * Constructs a new {@code ToggleAutoResolutionAction}.866 */867 public ToggleAutoResolutionAction() {868 super(tr("Automatically change resolution"));869 }870 871 @Override872 public void actionPerformed(ActionEvent ev) {873 JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();874 autoResolutionEnabled = checkbox.isSelected();875 }876 877 @Override878 public Component createMenuComponent() {879 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);880 item.setSelected(autoResolutionEnabled);881 return item;882 }883 884 @Override885 public boolean supportLayers(List<Layer> layers) {886 return layers.size() == 1 && layers.get(0) instanceof WMSLayer;887 }888 }889 890 /**891 72 * This action will add a WMS layer menu entry with the current WMS layer 892 73 * URL and name extended by the current resolution. 893 74 * When using the menu entry again, the WMS cache will be used properly. … … public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceC 905 86 } 906 87 } 907 88 908 private class StartStopAction extends AbstractAction implements LayerAction {909 910 public StartStopAction() {911 super(tr("Automatic downloading"));912 }913 914 @Override915 public Component createMenuComponent() {916 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);917 item.setSelected(autoDownloadEnabled);918 return item;919 }920 921 @Override922 public boolean supportLayers(List<Layer> layers) {923 return layers.size() == 1 && layers.get(0) instanceof WMSLayer;924 }925 926 @Override927 public void actionPerformed(ActionEvent e) {928 autoDownloadEnabled = !autoDownloadEnabled;929 if (autoDownloadEnabled) {930 for (int x = 0; x < dax; ++x) {931 for (int y = 0; y < day; ++y) {932 GeorefImage img = images[modulo(x,dax)][modulo(y,day)];933 if(img.getState() == State.NOT_IN_CACHE){934 addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), false, true));935 }936 }937 }938 Main.map.mapView.repaint();939 }940 }941 }942 943 private class ZoomToNativeResolution extends AbstractAction {944 945 public ZoomToNativeResolution() {946 super(tr("Zoom to native resolution"));947 }948 949 @Override950 public void actionPerformed(ActionEvent e) {951 Main.map.mapView.zoomTo(Main.map.mapView.getCenter(), 1 / info.getPixelPerDegree());952 }953 }954 955 private void cancelGrabberThreads(boolean wait) {956 requestQueueLock.lock();957 try {958 canceled = true;959 for (WMSGrabber grabber: grabbers) {960 grabber.cancel();961 }962 queueEmpty.signalAll();963 } finally {964 requestQueueLock.unlock();965 }966 if (wait) {967 for (Thread t: grabberThreads) {968 try {969 t.join();970 } catch (InterruptedException e) {971 Main.warn("InterruptedException in "+getClass().getSimpleName()+" while cancelling grabber threads");972 }973 }974 }975 }976 977 private void startGrabberThreads() {978 int threadCount = PROP_SIMULTANEOUS_CONNECTIONS.get();979 requestQueueLock.lock();980 try {981 canceled = false;982 grabbers.clear();983 grabberThreads.clear();984 for (int i=0; i<threadCount; i++) {985 WMSGrabber grabber = getGrabber(i == 0 && threadCount > 1);986 grabbers.add(grabber);987 Thread t = new Thread(grabber, "WMS " + getName() + " " + i);988 t.setDaemon(true);989 t.start();990 grabberThreads.add(t);991 }992 } finally {993 requestQueueLock.unlock();994 }995 }996 997 @Override998 public boolean isChanged() {999 requestQueueLock.lock();1000 try {1001 return !finishedRequests.isEmpty() || settingsChanged;1002 } finally {1003 requestQueueLock.unlock();1004 }1005 }1006 1007 @Override1008 public void preferenceChanged(PreferenceChangeEvent event) {1009 if (event.getKey().equals(PROP_SIMULTANEOUS_CONNECTIONS.getKey()) && info.getUrl() != null) {1010 cancelGrabberThreads(true);1011 startGrabberThreads();1012 } else if (1013 event.getKey().equals(PROP_OVERLAP.getKey())1014 || event.getKey().equals(PROP_OVERLAP_EAST.getKey())1015 || event.getKey().equals(PROP_OVERLAP_NORTH.getKey())) {1016 for (int i=0; i<images.length; i++) {1017 for (int k=0; k<images[i].length; k++) {1018 images[i][k] = new GeorefImage(this);1019 }1020 }1021 1022 settingsChanged = true;1023 }1024 }1025 89 1026 90 /** 1027 91 * Checks that WMS layer is a grabber-compatible one (HTML or WMS). … … public class WMSLayer extends ImageryLayer implements ImageObserver, PreferenceC 1029 93 * @since 8068 1030 94 */ 1031 95 public void checkGrabberType() { 1032 ImageryType it = getInfo().getImageryType();1033 if (!ImageryType.HTML.equals(it) && !ImageryType.WMS.equals(it))1034 throw new IllegalStateException("getGrabber() called for non-WMS layer type");1035 }1036 1037 protected WMSGrabber getGrabber(boolean localOnly) {1038 checkGrabberType();1039 if (getInfo().getImageryType() == ImageryType.HTML)1040 return new HTMLGrabber(Main.map.mapView, this, localOnly);1041 else1042 return new WMSGrabber(Main.map.mapView, this, localOnly);1043 }1044 1045 public ProjectionBounds getBounds(WMSRequest request) {1046 ProjectionBounds result = new ProjectionBounds(1047 getEastNorth(request.getXIndex(), request.getYIndex()),1048 getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));1049 1050 if (WMSLayer.PROP_OVERLAP.get()) {1051 double eastSize = result.maxEast - result.minEast;1052 double northSize = result.maxNorth - result.minNorth;1053 1054 double eastCoef = WMSLayer.PROP_OVERLAP_EAST.get() / 100.0;1055 double northCoef = WMSLayer.PROP_OVERLAP_NORTH.get() / 100.0;1056 1057 result = new ProjectionBounds(result.getMin(),1058 new EastNorth(result.maxEast + eastCoef * eastSize,1059 result.maxNorth + northCoef * northSize));1060 }1061 return result;1062 96 } 1063 97 98 private static TileLoaderFactory loaderFactory = new CachedTileLoaderFactory("WMS") { 1064 99 @Override 1065 public boolean isProjectionSupported(Projection proj) { 1066 List<String> serverProjections = info.getServerProjections(); 1067 return serverProjections.contains(proj.toCode().toUpperCase(Locale.ENGLISH)) 1068 || ("EPSG:3857".equals(proj.toCode()) && (serverProjections.contains("EPSG:4326") || serverProjections.contains("CRS:84"))) 1069 || ("EPSG:4326".equals(proj.toCode()) && serverProjections.contains("CRS:84")); 100 protected TileLoader getLoader(TileLoaderListener listener, String cacheName, int connectTimeout, 101 int readTimeout, Map<String, String> headers, String cacheDir) throws IOException { 102 return new WMSCachedTileLoader(listener, cacheName, connectTimeout, readTimeout, headers, cacheDir); 1070 103 } 1071 104 1072 @Override 1073 public String nameSupportedProjections() { 1074 StringBuilder res = new StringBuilder(); 1075 for (String p : info.getServerProjections()) { 1076 if (res.length() > 0) { 1077 res.append(", "); 1078 } 1079 res.append(p); 1080 } 1081 return tr("Supported projections are: {0}", res); 1082 } 1083 1084 @Override 1085 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { 1086 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0; 1087 Main.map.repaint(done ? 0 : 100); 1088 return !done; 1089 } 1090 1091 @Override 1092 public void writeExternal(ObjectOutput out) throws IOException { 1093 out.writeInt(serializeFormatVersion); 1094 out.writeInt(dax); 1095 out.writeInt(day); 1096 out.writeInt(imageSize); 1097 out.writeDouble(info.getPixelPerDegree()); 1098 out.writeObject(info.getName()); 1099 out.writeObject(info.getExtendedUrl()); 1100 out.writeObject(images); 1101 } 1102 1103 @Override 1104 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 1105 int sfv = in.readInt(); 1106 if (sfv != serializeFormatVersion) 1107 throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion)); 1108 autoDownloadEnabled = false; 1109 dax = in.readInt(); 1110 day = in.readInt(); 1111 imageSize = in.readInt(); 1112 info.setPixelPerDegree(in.readDouble()); 1113 doSetName((String)in.readObject()); 1114 info.setExtendedUrl((String)in.readObject()); 1115 images = (GeorefImage[][])in.readObject(); 1116 1117 for (GeorefImage[] imgs : images) { 1118 for (GeorefImage img : imgs) { 1119 if (img != null) { 1120 img.setLayer(WMSLayer.this); 1121 } 1122 } 1123 } 1124 1125 settingsChanged = true; 1126 if (Main.isDisplayingMapView()) { 1127 Main.map.mapView.repaint(); 1128 } 1129 if (cache != null) { 1130 cache.saveIndex(); 1131 cache = null; 1132 } 1133 } 105 }; 1134 106 1135 107 @Override 1136 public void onPostLoadFromFile() { 1137 if (info.getUrl() != null) { 1138 cache = new WmsCache(info.getUrl(), imageSize); 1139 startGrabberThreads(); 1140 } 108 protected TileLoaderFactory getTileLoaderFactory() { 109 return loaderFactory; 1141 110 } 1142 111 1143 112 @Override 1144 public boolean isSavable() { 1145 return true; // With WMSLayerExporter 113 protected Map<String, String> getHeaders(TileSource tileSource) { 114 if (tileSource instanceof TemplatedWMSTileSource) { 115 return ((TemplatedWMSTileSource)tileSource).getHeaders(); 1146 116 } 1147 1148 @Override 1149 public File createAndOpenSaveFileChooser() { 1150 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 117 return null; 1151 118 } 1152 119 } -
src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java
diff --git a/src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java b/src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java index ad63bbb..995cd17 100644
a b import org.openstreetmap.josm.data.gpx.GpxTrackSegment; 24 24 import org.openstreetmap.josm.data.gpx.WayPoint; 25 25 import org.openstreetmap.josm.gui.ExtendedDialog; 26 26 import org.openstreetmap.josm.gui.PleaseWaitRunnable; 27 import org.openstreetmap.josm.gui.layer. WMSLayer;28 import org.openstreetmap.josm.gui.layer. WMSLayer.PrecacheTask;27 import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 28 import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer.PrecacheTask; 29 29 import org.openstreetmap.josm.gui.progress.ProgressTaskId; 30 30 import org.openstreetmap.josm.gui.progress.ProgressTaskIds; 31 31 import org.openstreetmap.josm.gui.widgets.JosmComboBox; … … import org.openstreetmap.josm.tools.GBC; 34 34 import org.openstreetmap.josm.tools.ImageProvider; 35 35 import org.xml.sax.SAXException; 36 36 37 /** 38 * Class downloading WMS and TMS along the GPX track 39 * 40 */ 37 41 public class DownloadWmsAlongTrackAction extends AbstractAction { 38 42 39 43 private final transient GpxData data; 40 44 45 /** 46 * @param data that represents GPX track, along which data should be downloaded 47 */ 41 48 public DownloadWmsAlongTrackAction(final GpxData data) { 42 49 super(tr("Precache imagery tiles along this track"), ImageProvider.get("downloadalongtrack")); 43 50 this.data = data; … … public class DownloadWmsAlongTrackAction extends AbstractAction { 56 63 for (WayPoint p : data.waypoints) { 57 64 points.add(p.getCoor()); 58 65 } 59 final WMSLayer layer = askWMSLayer();66 final AbstractTileSourceLayer layer = askedLayer(); 60 67 if (layer != null) { 61 68 PleaseWaitRunnable task = new PleaseWaitRunnable(tr("Precaching WMS")) { 62 69 private PrecacheTask precacheTask; 63 70 64 71 @Override 65 72 protected void realRun() throws SAXException, IOException, OsmTransferException { 66 precacheTask = new PrecacheTask(progressMonitor);73 precacheTask = layer.new PrecacheTask(progressMonitor); 67 74 layer.downloadAreaToCache(precacheTask, points, 0, 0); 68 75 while (!precacheTask.isFinished() && !progressMonitor.isCanceled()) { 69 76 synchronized (this) { … … public class DownloadWmsAlongTrackAction extends AbstractAction { 94 101 } 95 102 } 96 103 97 protected WMSLayer askWMSLayer() {98 Collection< WMSLayer> targetLayers = Main.map.mapView.getLayersOfType(WMSLayer.class);104 protected AbstractTileSourceLayer askedLayer() { 105 Collection<AbstractTileSourceLayer> targetLayers = Main.map.mapView.getLayersOfType(AbstractTileSourceLayer.class); 99 106 if (targetLayers.isEmpty()) { 100 107 warnNoImageryLayers(); 101 108 return null; 102 109 } 103 JosmComboBox< WMSLayer> layerList = new JosmComboBox<>(targetLayers.toArray(new WMSLayer[0]));110 JosmComboBox<AbstractTileSourceLayer> layerList = new JosmComboBox<>(targetLayers.toArray(new AbstractTileSourceLayer[0])); 104 111 layerList.setRenderer(new LayerListCellRenderer()); 105 112 layerList.setSelectedIndex(0); 106 113 JPanel pnl = new JPanel(new GridBagLayout()); … … public class DownloadWmsAlongTrackAction extends AbstractAction { 113 120 if (ed.getValue() != 1) { 114 121 return null; 115 122 } 116 return ( WMSLayer) layerList.getSelectedItem();123 return (AbstractTileSourceLayer) layerList.getSelectedItem(); 117 124 } 118 125 119 126 protected void warnNoImageryLayers() { -
src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java
diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java index 96c2c59..ed5168b 100644
a b import javax.swing.JPanel; 11 11 import javax.swing.JSpinner; 12 12 import javax.swing.SpinnerNumberModel; 13 13 14 import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory; 14 15 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 15 16 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob; 16 17 import org.openstreetmap.josm.gui.layer.TMSLayer; … … public class TMSSettingsPanel extends JPanel { 40 41 */ 41 42 public TMSSettingsPanel() { 42 43 super(new GridBagLayout()); 43 minZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer. DEFAULT_MIN_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));44 maxZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer. DEFAULT_MAX_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));44 minZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.PROP_MIN_ZOOM_LVL.get().intValue(), TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1)); 45 maxZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.PROP_MAX_ZOOM_LVL.get().intValue(), TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1)); 45 46 maxElementsOnDisk = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.get().intValue(), 0, Integer.MAX_VALUE, 1)); 46 47 maxConcurrentDownloads = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoaderJob.THREAD_LIMIT.get().intValue(), 0, Integer.MAX_VALUE, 1)); 47 48 maxDownloadsPerHost = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoader.HOST_LIMIT.get().intValue(), 0, Integer.MAX_VALUE, 1)); … … public class TMSSettingsPanel extends JPanel { 94 95 this.addToSlippyMapChosser.setSelected(TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get()); 95 96 this.maxZoomLvl.setValue(TMSLayer.getMaxZoomLvl(null)); 96 97 this.minZoomLvl.setValue(TMSLayer.getMinZoomLvl(null)); 97 this.tilecacheDir.setText( TMSLayer.PROP_TILECACHE_DIR.get());98 this.tilecacheDir.setText(CachedTileLoaderFactory.PROP_TILECACHE_DIR.get()); 98 99 this.maxElementsOnDisk.setValue(TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.get()); 99 100 this.maxConcurrentDownloads.setValue(TMSCachedTileLoaderJob.THREAD_LIMIT.get()); 100 101 this.maxDownloadsPerHost.setValue(TMSCachedTileLoader.HOST_LIMIT.get()); … … public class TMSSettingsPanel extends JPanel { 131 132 restartRequired = true; 132 133 } 133 134 134 if (! TMSLayer.PROP_TILECACHE_DIR.get().equals(this.tilecacheDir.getText())) {135 if (!CachedTileLoaderFactory.PROP_TILECACHE_DIR.get().equals(this.tilecacheDir.getText())) { 135 136 restartRequired = true; 136 TMSLayer.PROP_TILECACHE_DIR.put(this.tilecacheDir.getText());137 CachedTileLoaderFactory.PROP_TILECACHE_DIR.put(this.tilecacheDir.getText()); 137 138 } 138 139 139 140 return restartRequired; -
src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java
diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/WMSSettingsPanel.java index 044c91b..dfe1e44 100644
a b package org.openstreetmap.josm.gui.preferences.imagery; 3 3 4 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 5 6 import java.awt.FlowLayout;7 6 import java.awt.GridBagLayout; 8 7 9 8 import javax.swing.Box; … … import javax.swing.JPanel; 13 12 import javax.swing.JSpinner; 14 13 import javax.swing.SpinnerNumberModel; 15 14 15 import org.openstreetmap.josm.data.imagery.WMSCachedTileLoaderJob; 16 16 import org.openstreetmap.josm.gui.layer.WMSLayer; 17 import org.openstreetmap.josm.gui.widgets.JosmComboBox;18 import org.openstreetmap.josm.io.imagery.HTMLGrabber;19 17 import org.openstreetmap.josm.tools.GBC; 20 18 21 19 /** … … public class WMSSettingsPanel extends JPanel { 26 24 27 25 // WMS Settings 28 26 private final JCheckBox autozoomActive; 29 private final JosmComboBox<String> browser;30 private final JCheckBox overlapCheckBox;31 private final JSpinner spinEast;32 private final JSpinner spinNorth;33 27 private final JSpinner spinSimConn; 28 private final JSpinner tileSize; 34 29 35 30 /** 36 31 * Constructs a new {@code WMSSettingsPanel}. … … public class WMSSettingsPanel extends JPanel { 44 39 add(GBC.glue(5, 0), GBC.std()); 45 40 add(autozoomActive, GBC.eol().fill(GBC.HORIZONTAL)); 46 41 47 // Downloader48 browser = new JosmComboBox<>(new String[] {49 "webkit-image {0}",50 "gnome-web-photo --mode=photo --format=png {0} /dev/stdout",51 "gnome-web-photo-fixed {0}",52 "webkit-image-gtk {0}"});53 browser.setEditable(true);54 add(new JLabel(tr("Downloader:")), GBC.std());55 add(GBC.glue(5, 0), GBC.std());56 add(browser, GBC.eol().fill(GBC.HORIZONTAL));57 58 42 // Simultaneous connections 59 43 add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL)); 60 44 JLabel labelSimConn = new JLabel(tr("Simultaneous connections:")); 61 spinSimConn = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.get().intValue(), 1, 30, 1)); 45 spinSimConn = new JSpinner(new SpinnerNumberModel(WMSCachedTileLoaderJob.THREAD_LIMIT.get().intValue(), 1, 30, 1)); 46 labelSimConn.setLabelFor(spinSimConn); 62 47 add(labelSimConn, GBC.std()); 63 48 add(GBC.glue(5, 0), GBC.std()); 64 49 add(spinSimConn, GBC.eol()); 65 50 66 // Overlap 67 add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL)); 68 69 overlapCheckBox = new JCheckBox(tr("Overlap tiles")); 70 JLabel labelEast = new JLabel(tr("% of east:")); 71 JLabel labelNorth = new JLabel(tr("% of north:")); 72 spinEast = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_OVERLAP_EAST.get().intValue(), 1, 50, 1)); 73 spinNorth = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_OVERLAP_NORTH.get().intValue(), 1, 50, 1)); 74 75 JPanel overlapPanel = new JPanel(new FlowLayout()); 76 overlapPanel.add(overlapCheckBox); 77 overlapPanel.add(labelEast); 78 overlapPanel.add(spinEast); 79 overlapPanel.add(labelNorth); 80 overlapPanel.add(spinNorth); 81 82 add(overlapPanel, GBC.eop()); 51 // Tile size 52 JLabel labelTileSize = new JLabel(tr("Tile size:")); 53 tileSize = new JSpinner(new SpinnerNumberModel(WMSLayer.PROP_IMAGE_SIZE.get().intValue(), 1, 4096, 128)); 54 labelTileSize.setLabelFor(tileSize); 55 add(labelTileSize, GBC.std()); 56 add(GBC.glue(5, 0), GBC.std()); 57 add(tileSize, GBC.eol()); 83 58 } 84 59 85 60 /** … … public class WMSSettingsPanel extends JPanel { 87 62 */ 88 63 public void loadSettings() { 89 64 this.autozoomActive.setSelected(WMSLayer.PROP_DEFAULT_AUTOZOOM.get()); 90 this.browser.setSelectedItem(HTMLGrabber.PROP_BROWSER.get()); 91 this.overlapCheckBox.setSelected(WMSLayer.PROP_OVERLAP.get()); 92 this.spinEast.setValue(WMSLayer.PROP_OVERLAP_EAST.get()); 93 this.spinNorth.setValue(WMSLayer.PROP_OVERLAP_NORTH.get()); 94 this.spinSimConn.setValue(WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.get()); 65 this.spinSimConn.setValue(WMSCachedTileLoaderJob.THREAD_LIMIT.get()); 66 this.tileSize.setValue(WMSLayer.PROP_IMAGE_SIZE.get()); 95 67 } 96 68 97 69 /** … … public class WMSSettingsPanel extends JPanel { 100 72 */ 101 73 public boolean saveSettings() { 102 74 WMSLayer.PROP_DEFAULT_AUTOZOOM.put(this.autozoomActive.isSelected()); 103 WMSLayer.PROP_OVERLAP.put(overlapCheckBox.getModel().isSelected()); 104 WMSLayer.PROP_OVERLAP_EAST.put((Integer) spinEast.getModel().getValue()); 105 WMSLayer.PROP_OVERLAP_NORTH.put((Integer) spinNorth.getModel().getValue()); 106 WMSLayer.PROP_SIMULTANEOUS_CONNECTIONS.put((Integer) spinSimConn.getModel().getValue()); 107 108 HTMLGrabber.PROP_BROWSER.put(browser.getEditor().getItem().toString()); 75 WMSCachedTileLoaderJob.THREAD_LIMIT.put((Integer) spinSimConn.getModel().getValue()); 76 WMSLayer.PROP_IMAGE_SIZE.put((Integer) this.tileSize.getModel().getValue()); 109 77 110 78 return false; 111 79 } -
src/org/openstreetmap/josm/io/WMSLayerExporter.java
diff --git a/src/org/openstreetmap/josm/io/WMSLayerExporter.java b/src/org/openstreetmap/josm/io/WMSLayerExporter.java index 4c8b1ed..15e765a 100644
a b import java.io.FileOutputStream; 6 6 import java.io.IOException; 7 7 import java.io.ObjectOutputStream; 8 8 9 import org.openstreetmap.josm.Main; 10 import org.openstreetmap.josm.data.Preferences; 11 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry; 12 import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 9 13 import org.openstreetmap.josm.gui.layer.Layer; 10 import org.openstreetmap.josm.gui.layer.WMSLayer;11 14 import org.openstreetmap.josm.tools.CheckParameterUtil; 12 15 13 16 /** … … import org.openstreetmap.josm.tools.CheckParameterUtil; 17 20 */ 18 21 public class WMSLayerExporter extends FileExporter { 19 22 23 /** Which version of the file we export */ 24 public static final int CURRENT_FILE_VERSION = 6; 25 20 26 /** 21 27 * Constructs a new {@code WMSLayerExporter} 22 28 */ … … public class WMSLayerExporter extends FileExporter { 28 34 public void exportData(File file, Layer layer) throws IOException { 29 35 CheckParameterUtil.ensureParameterNotNull(file, "file"); 30 36 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 31 if (layer instanceof WMSLayer) { 37 38 if (layer instanceof AbstractTileSourceLayer) { 32 39 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) { 33 ((WMSLayer)layer).writeExternal(oos); 40 oos.writeInt(CURRENT_FILE_VERSION); // file version 41 oos.writeObject(Main.map.mapView.getCenter()); 42 ImageryPreferenceEntry entry = new ImageryPreferenceEntry(((AbstractTileSourceLayer) layer).getInfo()); 43 oos.writeObject(Preferences.serializeStruct(entry, ImageryPreferenceEntry.class)); 34 44 } 35 45 } 46 36 47 } 37 48 38 49 @Override 39 50 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 40 setEnabled(newLayer instanceof WMSLayer);51 setEnabled(newLayer instanceof AbstractTileSourceLayer); 41 52 } 42 53 } -
src/org/openstreetmap/josm/io/WMSLayerImporter.java
diff --git a/src/org/openstreetmap/josm/io/WMSLayerImporter.java b/src/org/openstreetmap/josm/io/WMSLayerImporter.java index ea1daa3..df06db0 100644
a b import static org.openstreetmap.josm.tools.I18n.tr; 6 6 import java.io.File; 7 7 import java.io.FileInputStream; 8 8 import java.io.IOException; 9 import java.io.InvalidClassException; 9 10 import java.io.ObjectInputStream; 11 import java.util.Map; 10 12 11 13 import org.openstreetmap.josm.Main; 12 14 import org.openstreetmap.josm.actions.ExtensionFileFilter; 13 import org.openstreetmap.josm.gui.layer.WMSLayer; 15 import org.openstreetmap.josm.data.Preferences; 16 import org.openstreetmap.josm.data.coor.EastNorth; 17 import org.openstreetmap.josm.data.imagery.ImageryInfo; 18 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry; 19 import org.openstreetmap.josm.gui.layer.ImageryLayer; 14 20 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 15 21 import org.openstreetmap.josm.gui.util.GuiHelper; 16 22 import org.openstreetmap.josm.tools.CheckParameterUtil; … … public class WMSLayerImporter extends FileImporter { 27 33 public static final ExtensionFileFilter FILE_FILTER = new ExtensionFileFilter( 28 34 "wms", "wms", tr("WMS Files (*.wms)")); 29 35 30 private final WMSLayer wmsLayer;31 32 36 /** 33 37 * Constructs a new {@code WMSLayerImporter}. 34 38 */ 35 39 public WMSLayerImporter() { 36 this(new WMSLayer());37 }38 39 /**40 * Constructs a new {@code WMSLayerImporter} that will import data to the specified WMS layer.41 * @param wmsLayer The WMS layer.42 */43 public WMSLayerImporter(WMSLayer wmsLayer) {44 40 super(FILE_FILTER); 45 this.wmsLayer = wmsLayer;46 41 } 47 42 43 48 44 @Override 49 45 public void importData(File file, ProgressMonitor progressMonitor) throws IOException, IllegalDataException { 50 46 CheckParameterUtil.ensureParameterNotNull(file, "file"); 47 final EastNorth zoomTo; 48 ImageryInfo info = null; 49 final ImageryLayer layer; 50 51 51 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { 52 wmsLayer.readExternal(ois); 52 int sfv = ois.readInt(); 53 if (sfv < 5) { 54 throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, 5)); 55 } else if (sfv == 5) { 56 ois.readInt(); // dax - not needed 57 ois.readInt(); // day - not needed 58 zoomTo = null; 59 60 int imageSize = ois.readInt(); 61 double pixelPerDegree = ois.readDouble(); 62 63 String name = (String)ois.readObject(); 64 String extendedUrl = (String)ois.readObject(); 65 66 info = new ImageryInfo(name); 67 info.setExtendedUrl(extendedUrl); 68 info.setPixelPerDegree(pixelPerDegree); 69 info.setTileSize(imageSize); 70 } else if (sfv == WMSLayerExporter.CURRENT_FILE_VERSION){ 71 zoomTo = (EastNorth) ois.readObject(); 72 73 @SuppressWarnings("unchecked") 74 ImageryPreferenceEntry entry = Preferences.deserializeStruct( 75 (Map<String, String>)ois.readObject(), 76 ImageryPreferenceEntry.class); 77 info = new ImageryInfo(entry); 78 } else { 79 throw new InvalidClassException(tr("Unsupported WMS file version; found {0}, expected {1}", sfv, 6)); 80 } 53 81 } catch (ClassNotFoundException e) { 54 82 throw new IllegalDataException(e); 55 83 } 84 layer = ImageryLayer.create(info); 85 56 86 57 87 // FIXME: remove UI stuff from IO subsystem 58 88 GuiHelper.runInEDT(new Runnable() { 59 89 @Override 60 90 public void run() { 61 Main.main.addLayer(wmsLayer); 62 wmsLayer.onPostLoadFromFile(); 91 Main.main.addLayer(layer); 92 if (zoomTo != null) { 93 Main.map.mapView.zoomTo(zoomTo); 63 94 } 64 });65 95 } 66 67 /** 68 * Replies the imported WMS layer. 69 * @return The imported WMS layer. 70 * @see #importData(File, ProgressMonitor) 71 */ 72 public final WMSLayer getWmsLayer() { 73 return wmsLayer; 96 }); 74 97 } 75 98 } -
deleted file src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java
diff --git a/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java b/src/org/openstreetmap/josm/io/imagery/HTMLGrabber.java deleted file mode 100644 index 591e8ef..0000000
+ - 1 // License: GPL. For details, see LICENSE file.2 package org.openstreetmap.josm.io.imagery;3 4 import java.awt.image.BufferedImage;5 import java.io.ByteArrayInputStream;6 import java.io.ByteArrayOutputStream;7 import java.io.IOException;8 import java.net.URL;9 import java.text.MessageFormat;10 import java.util.ArrayList;11 import java.util.List;12 import java.util.StringTokenizer;13 14 import org.openstreetmap.josm.Main;15 import org.openstreetmap.josm.data.preferences.StringProperty;16 import org.openstreetmap.josm.gui.MapView;17 import org.openstreetmap.josm.gui.layer.WMSLayer;18 import org.openstreetmap.josm.tools.ImageProvider;19 import org.openstreetmap.josm.tools.Utils;20 21 public class HTMLGrabber extends WMSGrabber {22 public static final StringProperty PROP_BROWSER = new StringProperty("imagery.wms.browser", "webkit-image {0}");23 24 public HTMLGrabber(MapView mv, WMSLayer layer, boolean localOnly) {25 super(mv, layer, localOnly);26 }27 28 @Override29 protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws IOException {30 String urlstring = url.toExternalForm();31 32 Main.info("Grabbing HTML " + (attempt > 1? "(attempt " + attempt + ") ":"") + url);33 34 List<String> cmdParams = new ArrayList<>();35 StringTokenizer st = new StringTokenizer(MessageFormat.format(PROP_BROWSER.get(), urlstring));36 while (st.hasMoreTokens()) {37 cmdParams.add(st.nextToken());38 }39 40 ProcessBuilder builder = new ProcessBuilder( cmdParams);41 42 Process browser;43 try {44 browser = builder.start();45 } catch (IOException ioe) {46 throw new IOException("Could not start browser. Please check that the executable path is correct.\n" + ioe.getMessage(), ioe);47 }48 49 ByteArrayOutputStream baos = new ByteArrayOutputStream();50 Utils.copyStream(browser.getInputStream(), baos);51 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());52 BufferedImage img = layer.normalizeImage(ImageProvider.read(bais, true, WMSLayer.PROP_ALPHA_CHANNEL.get()));53 bais.reset();54 layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);55 56 return img;57 }58 } -
deleted file src/org/openstreetmap/josm/io/imagery/WMSException.java
diff --git a/src/org/openstreetmap/josm/io/imagery/WMSException.java b/src/org/openstreetmap/josm/io/imagery/WMSException.java deleted file mode 100644 index 9018b12..0000000
+ - 1 // License: GPL. For details, see LICENSE file.2 package org.openstreetmap.josm.io.imagery;3 4 import java.net.URL;5 import java.util.Arrays;6 import java.util.Collection;7 8 import org.openstreetmap.josm.tools.Utils;9 10 /**11 * WMS Service Exception, as defined by {@code application/vnd.ogc.se_xml} format:<ul>12 * <li><a href="http://schemas.opengis.net/wms/1.1.0/exception_1_1_0.dtd">WMS 1.1.0 DTD</a></li>13 * <li><a href="http://schemas.opengis.net/wms/1.3.0/exception_1_3_0.dtd">WMS 1.3.0 XSD</a></li>14 * </ul>15 * @since 742516 */17 public class WMSException extends Exception {18 19 private final transient WMSRequest request;20 private final URL url;21 private final String[] exceptions;22 23 /**24 * Constructs a new {@code WMSException}.25 * @param request the WMS request that lead to this exception26 * @param url the URL that lead to this exception27 * @param exceptions the exceptions replied by WMS server28 */29 public WMSException(WMSRequest request, URL url, Collection<String> exceptions) {30 super(Utils.join("\n", exceptions));31 this.request = request;32 this.url = url;33 this.exceptions = exceptions.toArray(new String[0]);34 }35 36 /**37 * Replies the WMS request that lead to this exception.38 * @return the WMS request39 */40 public final WMSRequest getRequest() {41 return request;42 }43 44 /**45 * Replies the URL that lead to this exception.46 * @return the URL47 */48 public final URL getUrl() {49 return url;50 }51 52 /**53 * Replies the WMS Service exceptions.54 * @return the exceptions55 */56 public final Collection<String> getExceptions() {57 return Arrays.asList(exceptions);58 }59 } -
deleted file src/org/openstreetmap/josm/io/imagery/WMSGrabber.java
diff --git a/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java b/src/org/openstreetmap/josm/io/imagery/WMSGrabber.java deleted file mode 100644 index f35b0c9..0000000
+ - 1 // License: GPL. For details, see LICENSE file.2 package org.openstreetmap.josm.io.imagery;3 4 import java.awt.image.BufferedImage;5 import java.io.BufferedReader;6 import java.io.ByteArrayInputStream;7 import java.io.ByteArrayOutputStream;8 import java.io.IOException;9 import java.io.InputStream;10 import java.io.InputStreamReader;11 import java.io.StringReader;12 import java.net.HttpURLConnection;13 import java.net.MalformedURLException;14 import java.net.URL;15 import java.net.URLConnection;16 import java.nio.charset.StandardCharsets;17 import java.text.DecimalFormat;18 import java.text.DecimalFormatSymbols;19 import java.text.NumberFormat;20 import java.util.ArrayList;21 import java.util.HashMap;22 import java.util.List;23 import java.util.Locale;24 import java.util.Map;25 import java.util.Map.Entry;26 import java.util.regex.Matcher;27 import java.util.regex.Pattern;28 29 import javax.xml.parsers.DocumentBuilder;30 import javax.xml.parsers.DocumentBuilderFactory;31 import javax.xml.parsers.ParserConfigurationException;32 33 import org.openstreetmap.josm.Main;34 import org.openstreetmap.josm.data.ProjectionBounds;35 import org.openstreetmap.josm.data.coor.EastNorth;36 import org.openstreetmap.josm.data.coor.LatLon;37 import org.openstreetmap.josm.data.imagery.GeorefImage.State;38 import org.openstreetmap.josm.data.imagery.ImageryInfo;39 import org.openstreetmap.josm.gui.MapView;40 import org.openstreetmap.josm.gui.layer.WMSLayer;41 import org.openstreetmap.josm.io.OsmTransferException;42 import org.openstreetmap.josm.io.ProgressInputStream;43 import org.openstreetmap.josm.tools.ImageProvider;44 import org.openstreetmap.josm.tools.Utils;45 import org.w3c.dom.Document;46 import org.w3c.dom.NodeList;47 import org.xml.sax.InputSource;48 import org.xml.sax.SAXException;49 50 /**51 * WMS grabber, fetching tiles from WMS server.52 * @since 371553 */54 public class WMSGrabber implements Runnable {55 56 protected final MapView mv;57 protected final WMSLayer layer;58 private final boolean localOnly;59 60 protected ProjectionBounds b;61 protected volatile boolean canceled;62 63 protected String baseURL;64 private ImageryInfo info;65 private Map<String, String> props = new HashMap<>();66 67 /**68 * Constructs a new {@code WMSGrabber}.69 * @param mv Map view70 * @param layer WMS layer71 */72 public WMSGrabber(MapView mv, WMSLayer layer, boolean localOnly) {73 this.mv = mv;74 this.layer = layer;75 this.localOnly = localOnly;76 this.info = layer.getInfo();77 this.baseURL = info.getUrl();78 if (layer.getInfo().getCookies() != null && !layer.getInfo().getCookies().isEmpty()) {79 props.put("Cookie", layer.getInfo().getCookies());80 }81 Pattern pattern = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}");82 StringBuffer output = new StringBuffer();83 Matcher matcher = pattern.matcher(this.baseURL);84 while (matcher.find()) {85 props.put(matcher.group(1),matcher.group(2));86 matcher.appendReplacement(output, "");87 }88 matcher.appendTail(output);89 this.baseURL = output.toString();90 }91 92 int width() {93 return layer.getBaseImageWidth();94 }95 96 int height() {97 return layer.getBaseImageHeight();98 }99 100 @Override101 public void run() {102 while (true) {103 if (canceled)104 return;105 WMSRequest request = layer.getRequest(localOnly);106 if (request == null)107 return;108 this.b = layer.getBounds(request);109 if (request.isPrecacheOnly()) {110 if (!layer.cache.hasExactMatch(Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth)) {111 attempt(request);112 } else if (Main.isDebugEnabled()) {113 Main.debug("Ignoring "+request+" (precache only + exact match)");114 }115 } else if (!loadFromCache(request)){116 attempt(request);117 } else if (Main.isDebugEnabled()) {118 Main.debug("Ignoring "+request+" (loaded from cache)");119 }120 layer.finishRequest(request);121 }122 }123 124 protected void attempt(WMSRequest request){ // try to fetch the image125 int maxTries = 5; // n tries for every image126 for (int i = 1; i <= maxTries; i++) {127 if (canceled)128 return;129 try {130 if (!request.isPrecacheOnly() && !layer.requestIsVisible(request))131 return;132 fetch(request, i);133 break; // break out of the retry loop134 } catch (IOException e) {135 try { // sleep some time and then ask the server again136 Thread.sleep(random(1000, 2000));137 } catch (InterruptedException e1) {138 Main.debug("InterruptedException in "+getClass().getSimpleName()+" during WMS request");139 }140 if (i == maxTries) {141 Main.error(e);142 request.finish(State.FAILED, null, null);143 }144 } catch (WMSException e) {145 // Fail fast in case of WMS Service exception: useless to retry:146 // either the URL is wrong or the server suffers huge problems147 Main.error("WMS service exception while requesting "+e.getUrl()+":\n"+e.getMessage().trim());148 request.finish(State.FAILED, null, e);149 break; // break out of the retry loop150 }151 }152 }153 154 public static int random(int min, int max) {155 return (int)(Math.random() * ((max+1)-min) ) + min;156 }157 158 public final void cancel() {159 canceled = true;160 }161 162 private void fetch(WMSRequest request, int attempt) throws IOException, WMSException {163 URL url = null;164 try {165 url = getURL(166 b.minEast, b.minNorth,167 b.maxEast, b.maxNorth,168 width(), height());169 request.finish(State.IMAGE, grab(request, url, attempt), null);170 171 } catch (IOException | OsmTransferException e) {172 Main.error(e);173 throw new IOException(e.getMessage() + "\nImage couldn't be fetched: " + (url != null ? url.toString() : ""), e);174 }175 }176 177 public static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));178 179 protected URL getURL(double w, double s,double e,double n,180 int wi, int ht) throws MalformedURLException {181 String myProj = Main.getProjection().toCode();182 if (!info.getServerProjections().contains(myProj) && "EPSG:3857".equals(Main.getProjection().toCode())) {183 LatLon sw = Main.getProjection().eastNorth2latlon(new EastNorth(w, s));184 LatLon ne = Main.getProjection().eastNorth2latlon(new EastNorth(e, n));185 myProj = "EPSG:4326";186 s = sw.lat();187 w = sw.lon();188 n = ne.lat();189 e = ne.lon();190 }191 if ("EPSG:4326".equals(myProj) && !info.getServerProjections().contains(myProj) && info.getServerProjections().contains("CRS:84")) {192 myProj = "CRS:84";193 }194 195 // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.196 //197 // Background:198 //199 // bbox=x_min,y_min,x_max,y_max200 //201 // SRS=... is WMS 1.1.1202 // CRS=... is WMS 1.3.0203 //204 // The difference:205 // For SRS x is east-west and y is north-south206 // For CRS x and y are as specified by the EPSG207 // E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326.208 // For most other EPSG code there seems to be no difference.209 // [1] https://www.epsg-registry.org/report.htm?type=selection&entity=urn:ogc:def:crs:EPSG::4326&reportDetail=short&style=urn:uuid:report-style:default-with-code&style_name=OGP%20Default%20With%20Code&title=EPSG:4326210 boolean switchLatLon = false;211 if (baseURL.toLowerCase(Locale.ENGLISH).contains("crs=epsg:4326")) {212 switchLatLon = true;213 } else if (baseURL.toLowerCase(Locale.ENGLISH).contains("crs=") && "EPSG:4326".equals(myProj)) {214 switchLatLon = true;215 }216 String bbox;217 if (switchLatLon) {218 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e));219 } else {220 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n));221 }222 return new URL(baseURL.replaceAll("\\{proj(\\([^})]+\\))?\\}", myProj)223 .replaceAll("\\{bbox\\}", bbox)224 .replaceAll("\\{w\\}", latLonFormat.format(w))225 .replaceAll("\\{s\\}", latLonFormat.format(s))226 .replaceAll("\\{e\\}", latLonFormat.format(e))227 .replaceAll("\\{n\\}", latLonFormat.format(n))228 .replaceAll("\\{width\\}", String.valueOf(wi))229 .replaceAll("\\{height\\}", String.valueOf(ht))230 .replace(" ", "%20"));231 }232 233 public boolean loadFromCache(WMSRequest request) {234 BufferedImage cached = layer.cache.getExactMatch(235 Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);236 237 if (cached != null) {238 request.finish(State.IMAGE, cached, null);239 return true;240 } else if (request.isAllowPartialCacheMatch()) {241 BufferedImage partialMatch = layer.cache.getPartialMatch(242 Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);243 if (partialMatch != null) {244 request.finish(State.PARTLY_IN_CACHE, partialMatch, null);245 return true;246 }247 }248 249 if (!request.isReal() && !layer.hasAutoDownload()){250 request.finish(State.NOT_IN_CACHE, null, null);251 return true;252 }253 254 return false;255 }256 257 protected BufferedImage grab(WMSRequest request, URL url, int attempt) throws WMSException, IOException, OsmTransferException {258 Main.info("Grabbing WMS " + (attempt > 1? "(attempt " + attempt + ") ":"") + url);259 260 HttpURLConnection conn = Utils.openHttpConnection(url);261 conn.setUseCaches(true);262 for (Entry<String, String> e : props.entrySet()) {263 conn.setRequestProperty(e.getKey(), e.getValue());264 }265 conn.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15) * 1000);266 conn.setReadTimeout(Main.pref.getInteger("socket.timeout.read", 30) * 1000);267 268 String contentType = conn.getHeaderField("Content-Type");269 if (conn.getResponseCode() != 200270 || contentType != null && !contentType.startsWith("image") ) {271 String xml = readException(conn);272 try {273 DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();274 InputSource is = new InputSource(new StringReader(xml));275 Document doc = db.parse(is);276 NodeList nodes = doc.getElementsByTagName("ServiceException");277 List<String> exceptions = new ArrayList<>(nodes.getLength());278 for (int i = 0; i < nodes.getLength(); i++) {279 exceptions.add(nodes.item(i).getTextContent());280 }281 throw new WMSException(request, url, exceptions);282 } catch (SAXException | ParserConfigurationException ex) {283 throw new IOException(xml, ex);284 }285 }286 287 ByteArrayOutputStream baos = new ByteArrayOutputStream();288 try (InputStream is = new ProgressInputStream(conn, null)) {289 Utils.copyStream(is, baos);290 }291 292 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());293 BufferedImage img = layer.normalizeImage(ImageProvider.read(bais, true, WMSLayer.PROP_ALPHA_CHANNEL.get()));294 bais.reset();295 layer.cache.saveToCache(layer.isOverlapEnabled()?img:null, bais, Main.getProjection(), request.getPixelPerDegree(), b.minEast, b.minNorth);296 return img;297 }298 299 protected String readException(URLConnection conn) throws IOException {300 StringBuilder exception = new StringBuilder();301 InputStream in = conn.getInputStream();302 try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {303 String line = null;304 while( (line = br.readLine()) != null) {305 // filter non-ASCII characters and control characters306 exception.append(line.replaceAll("[^\\p{Print}]", ""));307 exception.append('\n');308 }309 return exception.toString();310 }311 }312 } -
deleted file src/org/openstreetmap/josm/io/imagery/WMSRequest.java
diff --git a/src/org/openstreetmap/josm/io/imagery/WMSRequest.java b/src/org/openstreetmap/josm/io/imagery/WMSRequest.java deleted file mode 100644 index db25eb1..0000000
+ - 1 // License: GPL. For details, see LICENSE file.2 package org.openstreetmap.josm.io.imagery;3 4 import java.awt.image.BufferedImage;5 6 import org.openstreetmap.josm.data.imagery.GeorefImage.State;7 import org.openstreetmap.josm.gui.layer.WMSLayer.PrecacheTask;8 9 public class WMSRequest implements Comparable<WMSRequest> {10 private final int xIndex;11 private final int yIndex;12 private final double pixelPerDegree;13 private final boolean real; // Download even if autodownloading is disabled14 private final PrecacheTask precacheTask; // Download even when wms tile is not currently visible (precache)15 private final boolean allowPartialCacheMatch;16 private int priority;17 private boolean hasExactMatch;18 // Result19 private State state;20 private BufferedImage image;21 private WMSException exception;22 23 public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real, boolean allowPartialCacheMatch) {24 this(xIndex, yIndex, pixelPerDegree, real, allowPartialCacheMatch, null);25 }26 27 public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real, boolean allowPartialCacheMatch, PrecacheTask precacheTask) {28 this.xIndex = xIndex;29 this.yIndex = yIndex;30 this.pixelPerDegree = pixelPerDegree;31 this.real = real;32 this.precacheTask = precacheTask;33 this.allowPartialCacheMatch = allowPartialCacheMatch;34 }35 36 public void finish(State state, BufferedImage image, WMSException exception) {37 this.state = state;38 this.image = image;39 this.exception = exception;40 }41 42 public int getXIndex() {43 return xIndex;44 }45 46 public int getYIndex() {47 return yIndex;48 }49 50 public double getPixelPerDegree() {51 return pixelPerDegree;52 }53 54 @Override55 public int hashCode() {56 final int prime = 31;57 int result = 1;58 long temp;59 temp = Double.doubleToLongBits(pixelPerDegree);60 result = prime * result + (int) (temp ^ (temp >>> 32));61 result = prime * result + xIndex;62 result = prime * result + yIndex;63 return result;64 }65 66 @Override67 public boolean equals(Object obj) {68 if (this == obj)69 return true;70 if (obj == null)71 return false;72 if (getClass() != obj.getClass())73 return false;74 WMSRequest other = (WMSRequest) obj;75 if (Double.doubleToLongBits(pixelPerDegree) != Double76 .doubleToLongBits(other.pixelPerDegree))77 return false;78 if (xIndex != other.xIndex)79 return false;80 if (yIndex != other.yIndex)81 return false;82 if (allowPartialCacheMatch != other.allowPartialCacheMatch)83 return false;84 return true;85 }86 87 public void setPriority(int priority) {88 this.priority = priority;89 }90 91 public int getPriority() {92 return priority;93 }94 95 @Override96 public int compareTo(WMSRequest o) {97 return priority - o.priority;98 }99 100 /**101 * Replies the resulting state.102 * @return the resulting state103 */104 public State getState() {105 return state;106 }107 108 /**109 * Replies the resulting image, if any.110 * @return the resulting image, or {@code null}111 */112 public BufferedImage getImage() {113 return image;114 }115 116 /**117 * Replies the resulting exception, if any.118 * @return the resulting exception, or {@code null}119 * @since 7425120 */121 public WMSException getException() {122 return exception;123 }124 125 @Override126 public String toString() {127 return "WMSRequest [xIndex=" + xIndex + ", yIndex=" + yIndex128 + ", pixelPerDegree=" + pixelPerDegree + "]";129 }130 131 public boolean isReal() {132 return real;133 }134 135 public boolean isPrecacheOnly() {136 return precacheTask != null;137 }138 139 public PrecacheTask getPrecacheTask() {140 return precacheTask;141 }142 143 public boolean isAllowPartialCacheMatch() {144 return allowPartialCacheMatch;145 }146 147 public boolean hasExactMatch() {148 return hasExactMatch;149 }150 151 public void setHasExactMatch(boolean hasExactMatch) {152 this.hasExactMatch = hasExactMatch;153 }154 } -
src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java
diff --git a/src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java b/src/org/openstreetmap/josm/io/session/ImagerySessionExporter.java index da5ce92..ac621af 100644
a b import javax.swing.SwingConstants; 16 16 17 17 import org.openstreetmap.josm.data.Preferences; 18 18 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry; 19 import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 19 20 import org.openstreetmap.josm.gui.layer.ImageryLayer; 20 21 import org.openstreetmap.josm.gui.layer.Layer; 21 22 import org.openstreetmap.josm.gui.layer.TMSLayer; … … public class ImagerySessionExporter implements SessionLayerExporter { 79 80 layerElem.setAttribute("version", "0.1"); 80 81 ImageryPreferenceEntry e = new ImageryPreferenceEntry(layer.getInfo()); 81 82 Map<String,String> data = new LinkedHashMap<>(Preferences.serializeStruct(e, ImageryPreferenceEntry.class)); 82 if (layer instanceof WMSLayer) { 83 WMSLayer wms = (WMSLayer) layer; 84 data.put("automatic-downloading", Boolean.toString(wms.hasAutoDownload())); 85 data.put("automatically-change-resolution", Boolean.toString(wms.isAutoResolution())); 83 if (layer instanceof AbstractTileSourceLayer) { 84 AbstractTileSourceLayer tsLayer = (AbstractTileSourceLayer) layer; 85 data.put("automatic-downloading", Boolean.toString(tsLayer.autoLoad)); 86 data.put("automatically-change-resolution", Boolean.toString(tsLayer.autoZoom)); 87 data.put("show-errors", Boolean.toString(tsLayer.showErrors)); 86 88 } 87 89 for (Map.Entry<String,String> entry : data.entrySet()) { 88 90 Element attrElem = support.createElement(entry.getKey()); -
src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java
diff --git a/src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java b/src/org/openstreetmap/josm/io/session/ImagerySessionImporter.java index 95d48ec..3f7346d 100644
a b import java.util.Map; 10 10 import org.openstreetmap.josm.data.Preferences; 11 11 import org.openstreetmap.josm.data.imagery.ImageryInfo; 12 12 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry; 13 import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 13 14 import org.openstreetmap.josm.gui.layer.ImageryLayer; 14 15 import org.openstreetmap.josm.gui.layer.Layer; 15 import org.openstreetmap.josm.gui.layer.WMSLayer;16 16 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 17 17 import org.openstreetmap.josm.io.IllegalDataException; 18 18 import org.openstreetmap.josm.io.session.SessionReader.ImportSupport; … … public class ImagerySessionImporter implements SessionLayerImporter { 45 45 ImageryPreferenceEntry prefEntry = Preferences.deserializeStruct(attributes, ImageryPreferenceEntry.class); 46 46 ImageryInfo i = new ImageryInfo(prefEntry); 47 47 ImageryLayer layer = ImageryLayer.create(i); 48 if (layer instanceof WMSLayer) { 49 WMSLayer wms = (WMSLayer) layer; 50 String autoDownload = attributes.get("automatic-downloading"); 51 if (autoDownload != null) { 52 wms.setAutoDownload(Boolean.parseBoolean(autoDownload)); 53 } 54 String autoResolution = attributes.get("automatically-change-resolution"); 55 if (autoResolution != null) { 56 wms.setAutoResolution(Boolean.parseBoolean(autoResolution)); 48 if (layer instanceof AbstractTileSourceLayer) { 49 AbstractTileSourceLayer tsLayer = (AbstractTileSourceLayer) layer; 50 if (attributes.containsKey("automatic-downloading")) { 51 tsLayer.autoLoad = new Boolean(attributes.get("automatic-downloading")).booleanValue(); 52 } 53 54 if (attributes.containsKey("automatically-change-resolution")) { 55 tsLayer.autoZoom = new Boolean(attributes.get("automatically-change-resolution")).booleanValue(); 56 } 57 58 if (attributes.containsKey("show-errors")) { 59 tsLayer.showErrors = new Boolean(attributes.get("show-errors")).booleanValue(); 57 60 } 58 61 } 59 62 return layer;
