gt:         tileOptionMenu.add(new JMenuItem(new AbstractAction(
context: // License: GPL. For details, see LICENSE file.
  package org.openstreetmap.josm.gui.layer;
  
  import static org.openstreetmap.josm.tools.I18n.tr;
  
  import java.awt.Color;
  import java.awt.Font;
  import java.awt.Graphics;
  import java.awt.Graphics2D;
  import java.awt.Image;
  import java.awt.Point;
  import java.awt.Rectangle;
  import java.awt.Toolkit;
  import java.awt.event.ActionEvent;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.awt.image.ImageObserver;
  import java.io.File;
  import java.io.IOException;
  import java.io.StringReader;
  import java.net.URL;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Scanner;
  import java.util.Set;
  import java.util.concurrent.Callable;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import javax.swing.AbstractAction;
  import javax.swing.Action;
  import javax.swing.JCheckBoxMenuItem;
  import javax.swing.JMenuItem;
  import javax.swing.JOptionPane;
  import javax.swing.JPopupMenu;
  
  import org.openstreetmap.gui.jmapviewer.AttributionSupport;
  import org.openstreetmap.gui.jmapviewer.Coordinate;
  import org.openstreetmap.gui.jmapviewer.JobDispatcher;
  import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
  import org.openstreetmap.gui.jmapviewer.OsmFileCacheTileLoader;
  import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
  import org.openstreetmap.gui.jmapviewer.Tile;
  import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
  import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
  import org.openstreetmap.gui.jmapviewer.interfaces.TileClearController;
  import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
  import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
  import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
  import org.openstreetmap.gui.jmapviewer.tilesources.ScanexTileSource;
  import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
  import org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource;
  import org.openstreetmap.josm.Main;
  import org.openstreetmap.josm.actions.RenameLayerAction;
  import org.openstreetmap.josm.data.Bounds;
  import org.openstreetmap.josm.data.Version;
  import org.openstreetmap.josm.data.coor.EastNorth;
  import org.openstreetmap.josm.data.coor.LatLon;
  import org.openstreetmap.josm.data.imagery.ImageryInfo;
  import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
  import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
  import org.openstreetmap.josm.data.preferences.BooleanProperty;
  import org.openstreetmap.josm.data.preferences.IntegerProperty;
  import org.openstreetmap.josm.data.preferences.StringProperty;
  import org.openstreetmap.josm.data.projection.Projection;
  import org.openstreetmap.josm.gui.MapFrame;
  import org.openstreetmap.josm.gui.MapView;
  import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
  import org.openstreetmap.josm.gui.PleaseWaitRunnable;
  import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
  import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
  import org.openstreetmap.josm.gui.progress.ProgressMonitor;
  import org.openstreetmap.josm.gui.progress.ProgressMonitor.CancelListener;
  import org.openstreetmap.josm.io.CacheCustomContent;
  import org.openstreetmap.josm.io.OsmTransferException;
  import org.openstreetmap.josm.io.UTFInputStreamReader;
  import org.openstreetmap.josm.tools.Utils;
  import org.xml.sax.InputSource;
  import org.xml.sax.SAXException;
  
  /**
   * Class that displays a slippy map layer.
   *
   * @author Frederik Ramm
   * @author LuVar &lt;lubomir.varga@freemap.sk&gt;
   * @author Dave Hansen &lt;dave@sr71.net&gt;
   * @author Upliner &lt;upliner@gmail.com&gt;
   *
   */
  public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener {
      public static final String PREFERENCE_PREFIX   = "imagery.tms";
  
      public static final int MAX_ZOOM = 30;
      public static final int MIN_ZOOM = 2;
      public static final int DEFAULT_MAX_ZOOM = 20;
      public static final int DEFAULT_MIN_ZOOM = 2;
  
      public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
      public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
      public static final BooleanProperty PROP_DEFAULT_SHOWERRORS = new BooleanProperty(PREFERENCE_PREFIX + ".default_showerrors", true);
      public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", DEFAULT_MIN_ZOOM);
      public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM);
      //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
      public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true);
      public static final IntegerProperty PROP_TMS_JOBS = new IntegerProperty("tmsloader.maxjobs", 25);
      public static final StringProperty PROP_TILECACHE_DIR;
  
      static {
          String defPath = null;
          try {
              defPath = OsmFileCacheTileLoader.getDefaultCacheDir().getAbsolutePath();
          } catch (SecurityException e) {
              Main.warn(e);
          }
          PROP_TILECACHE_DIR = new StringProperty(PREFERENCE_PREFIX + ".tilecache_path", defPath);
      }
  
      /*boolean debug = true;*/
  
      public interface TileLoaderFactory {
          OsmTileLoader makeTileLoader(TileLoaderListener listener);
      }
  
      protected MemoryTileCache tileCache;
      protected TileSource tileSource;
      protected OsmTileLoader tileLoader;
  
      public static TileLoaderFactory loaderFactory = new TileLoaderFactory() {
          @Override
          public OsmTileLoader makeTileLoader(TileLoaderListener listener) {
              String cachePath = TMSLayer.PROP_TILECACHE_DIR.get();
              if (cachePath != null && !cachePath.isEmpty()) {
                  try {
                      OsmFileCacheTileLoader loader = new OsmFileCacheTileLoader(listener, new File(cachePath));
                      loader.headers.put("User-Agent", Version.getInstance().getFullAgentString());
                      return loader;
                  } catch (IOException e) {
                      Main.warn(e);
                  }
              }
              return null;
          }
      };
  
      /**
       * Plugins that wish to set custom tile loader should call this method
       */
      public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) {
          TMSLayer.loaderFactory = loaderFactory;
      }
  
      private Set<Tile> tileRequestsOutstanding = new HashSet<Tile>();
  
      @Override
      public synchronized void tileLoadingFinished(Tile tile, boolean success) {
          if (tile.hasError()) {
              success = false;
              tile.setImage(null);
          }
          if (sharpenLevel != 0 && success) {
              tile.setImage(sharpenImage(tile.getImage()));
          }
          tile.setLoaded(true);
          needRedraw = true;
          Main.map.repaint(100);
          tileRequestsOutstanding.remove(tile);
          /*if (debug) {
              Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
          }*/
      }
  
      @Override
      public TileCache getTileCache() {
          return tileCache;
      }
  
      private static class TmsTileClearController implements TileClearController, CancelListener {
  
          private final ProgressMonitor monitor;
          private boolean cancel = false;
  
          public TmsTileClearController(ProgressMonitor monitor) {
              this.monitor = monitor;
              this.monitor.addCancelListener(this);
          }
  
          @Override
          public void initClearDir(File dir) {
          }
  
          @Override
          public void initClearFiles(File[] files) {
              monitor.setTicksCount(files.length);
              monitor.setTicks(0);
          }
  
          @Override
          public boolean cancel() {
              return cancel;
          }
  
          @Override
          public void fileDeleted(File file) {
              monitor.setTicks(monitor.getTicks()+1);
          }
  
          @Override
          public void clearFinished() {
              monitor.finishTask();
          }
  
          @Override
          public void operationCanceled() {
              cancel = true;
          }
      }
  
      /**
       * Clears the tile cache.
       *
       * If the current tileLoader is an instance of OsmTileLoader, a new
       * TmsTileClearController is created and passed to the according clearCache
       * method.
       *
       * @param monitor
       * @see MemoryTileCache#clear()
       * @see OsmFileCacheTileLoader#clearCache(org.openstreetmap.gui.jmapviewer.interfaces.TileSource, org.openstreetmap.gui.jmapviewer.OsmFileCacheTileLoader.TileClearController)
       */
      void clearTileCache(ProgressMonitor monitor) {
          tileCache.clear();
          if (tileLoader instanceof CachedTileLoader) {
              ((CachedTileLoader)tileLoader).clearCache(tileSource, new TmsTileClearController(monitor));
          }
      }
  
      /**
       * Zoomlevel at which tiles is currently downloaded.
       * Initial zoom lvl is set to bestZoom
       */
      public int currentZoomLevel;
  
      private Tile clickedTile;
      private boolean needRedraw;
      private JPopupMenu tileOptionMenu;
      JCheckBoxMenuItem autoZoomPopup;
      JCheckBoxMenuItem autoLoadPopup;
      JCheckBoxMenuItem showErrorsPopup;
      Tile showMetadataTile;
      private AttributionSupport attribution = new AttributionSupport();
      private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
  
      protected boolean autoZoom;
      protected boolean autoLoad;
      protected boolean showErrors;
  
      /**
       * Initiates a repaint of Main.map
       *
       * @see Main#map
       * @see MapFrame#repaint()
       */
      void redraw() {
          needRedraw = true;
          Main.map.repaint();
      }
  
      static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
          if(maxZoomLvl > MAX_ZOOM) {
              maxZoomLvl = MAX_ZOOM;
          }
          if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
              maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
          }
          if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
              maxZoomLvl = ts.getMaxZoom();
          }
          return maxZoomLvl;
      }
  
      public static int getMaxZoomLvl(TileSource ts) {
          return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
      }
  
      public static void setMaxZoomLvl(int maxZoomLvl) {
          maxZoomLvl = checkMaxZoomLvl(maxZoomLvl, null);
          PROP_MAX_ZOOM_LVL.put(maxZoomLvl);
      }
  
      static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
          if(minZoomLvl < MIN_ZOOM) {
              /*Main.debug("Min. zoom level should not be less than "+MIN_ZOOM+"! Setting to that.");*/
              minZoomLvl = MIN_ZOOM;
          }
          if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
              /*Main.debug("Min. zoom level should not be more than Max. zoom level! Setting to Max.");*/
              minZoomLvl = getMaxZoomLvl(ts);
          }
          if (ts != null && ts.getMinZoom() > minZoomLvl) {
              /*Main.debug("Increasing min. zoom level to match tile source");*/
              minZoomLvl = ts.getMinZoom();
          }
          return minZoomLvl;
      }
  
      public static int getMinZoomLvl(TileSource ts) {
          return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
      }
  
      public static void setMinZoomLvl(int minZoomLvl) {
          minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
          PROP_MIN_ZOOM_LVL.put(minZoomLvl);
      }
  
      private static class CachedAttributionBingAerialTileSource extends BingAerialTileSource {
  
          class BingAttributionData extends CacheCustomContent<IOException> {
  
              public BingAttributionData() {
                  super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY);
              }
  
              @Override
              protected byte[] updateData() throws IOException {
                  URL u = getAttributionUrl();
                  UTFInputStreamReader in = UTFInputStreamReader.create(Utils.openURL(u));
                  String r = new Scanner(in).useDelimiter("\\A").next();
                  Utils.close(in);
                  Main.info("Successfully loaded Bing attribution data.");
                  return r.getBytes("utf-8");
              }
          }
  
          @Override
          protected Callable<List<Attribution>> getAttributionLoaderCallable() {
              return new Callable<List<Attribution>>() {
  
                  @Override
                  public List<Attribution> call() throws Exception {
                      BingAttributionData attributionLoader = new BingAttributionData();
                      int waitTimeSec = 1;
                      while (true) {
                          try {
                              String xml = attributionLoader.updateIfRequiredString();
                              return parseAttributionText(new InputSource(new StringReader((xml))));
                          } catch (IOException ex) {
                              Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
                              Thread.sleep(waitTimeSec * 1000L);
                              waitTimeSec *= 2;
                          }
                      }
                  }
              };
          }
      }
  
      /**
       * Creates and returns a new TileSource instance depending on the {@link ImageryType}
       * of the passed ImageryInfo object.
       *
       * If no appropriate TileSource is found, null is returned.
       * Currently supported ImageryType are {@link ImageryType#TMS},
       * {@link ImageryType#BING}, {@link ImageryType#SCANEX}.
       *
       * @param info
       * @return a new TileSource instance or null if no TileSource for the ImageryInfo/ImageryType could be found.
       * @throws IllegalArgumentException
       */
      public static TileSource getTileSource(ImageryInfo info) throws IllegalArgumentException {
          if (info.getImageryType() == ImageryType.TMS) {
              checkUrl(info.getUrl());
              TMSTileSource t = new TemplatedTMSTileSource(info.getName(), info.getUrl(), info.getMinZoom(), info.getMaxZoom());
              info.setAttribution(t);
              return t;
          } else if (info.getImageryType() == ImageryType.BING)
              return new CachedAttributionBingAerialTileSource();
          else if (info.getImageryType() == ImageryType.SCANEX) {
              return new ScanexTileSource(info.getName(), info.getUrl(), info.getMaxZoom());
          }
          return null;
      }
  
      public static void checkUrl(String url) throws IllegalArgumentException {
          if (url == null) {
              throw new IllegalArgumentException();
          } else {
              Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
              while (m.find()) {
                  boolean isSupportedPattern = false;
                  for (String pattern : TemplatedTMSTileSource.ALL_PATTERNS) {
                      if (m.group().matches(pattern)) {
                          isSupportedPattern = true;
                          break;
                      }
                  }
                  if (!isSupportedPattern) {
                      throw new IllegalArgumentException(tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url));
                  }
              }
          }
      }
  
      private void initTileSource(TileSource tileSource) {
          this.tileSource = tileSource;
          attribution.initialize(tileSource);
  
          currentZoomLevel = getBestZoom();
  
          tileCache = new MemoryTileCache();
  
          tileLoader = loaderFactory.makeTileLoader(this);
          if (tileLoader == null) {
              tileLoader = new OsmTileLoader(this);
          }
          tileLoader.timeoutConnect = Main.pref.getInteger("socket.timeout.connect",15) * 1000;
          tileLoader.timeoutRead = Main.pref.getInteger("socket.timeout.read", 30) * 1000;
          if (tileSource instanceof TemplatedTMSTileSource) {
              for(Entry<String, String> e : ((TemplatedTMSTileSource)tileSource).getHeaders().entrySet()) {
                  tileLoader.headers.put(e.getKey(), e.getValue());
              }
          }
          tileLoader.headers.put("User-Agent", Version.getInstance().getFullAgentString());
      }
  
      @Override
      public void setOffset(double dx, double dy) {
          super.setOffset(dx, dy);
          needRedraw = true;
      }
  
      /**
       * Returns average number of screen pixels per tile pixel for current mapview
       */
      private double getScaleFactor(int zoom) {
          if (!Main.isDisplayingMapView()) return 1;
          MapView mv = Main.map.mapView;
          LatLon topLeft = mv.getLatLon(0, 0);
          LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
          double x1 = tileSource.lonToTileX(topLeft.lon(), zoom);
          double y1 = tileSource.latToTileY(topLeft.lat(), zoom);
          double x2 = tileSource.lonToTileX(botRight.lon(), zoom);
          double y2 = tileSource.latToTileY(botRight.lat(), zoom);
  
          int screenPixels = mv.getWidth()*mv.getHeight();
          double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
          if (screenPixels == 0 || tilePixels == 0) return 1;
          return screenPixels/tilePixels;
      }
  
      private int getBestZoom() {
          double factor = getScaleFactor(1);
          double result = Math.log(factor)/Math.log(2)/2+1;
          // In general, smaller zoom levels are more readable.  We prefer big,
          // block, pixelated (but readable) map text to small, smeared,
          // unreadable underzoomed text.  So, use .floor() instead of rounding
          // to skew things a bit toward the lower zooms.
          int intResult = (int)Math.floor(result);
          if (intResult > getMaxZoomLvl())
              return getMaxZoomLvl();
          if (intResult < getMinZoomLvl())
              return getMinZoomLvl();
          return intResult;
      }
  
      /**
       * Function to set the maximum number of workers for tile loading to the value defined
       * in preferences.
       */
      static public void setMaxWorkers() {
          JobDispatcher.setMaxWorkers(PROP_TMS_JOBS.get());
          JobDispatcher.getInstance().setLIFO(true);
      }
  
      @SuppressWarnings("serial")
      public TMSLayer(ImageryInfo info) {
          super(info);
  
          setMaxWorkers();
          if(!isProjectionSupported(Main.getProjection())) {
              JOptionPane.showMessageDialog(Main.parent,
                  tr("TMS layers do not support the projection {0}.\n{1}\n"
                  + "Change the projection or remove the layer.",
                  Main.getProjection().toCode(), nameSupportedProjections()),
                  tr("Warning"),
                  JOptionPane.WARNING_MESSAGE);
          }
  
          setBackgroundLayer(true);
          this.setVisible(true);
  
          TileSource source = getTileSource(info);
          if (source == null)
              throw new IllegalStateException("Cannot create TMSLayer with non-TMS ImageryInfo");
          initTileSource(source);
      }
  
      /**
       * Adds a context menu to the mapView.
       */
      @Override
      public void hookUpMapView() {
          tileOptionMenu = new JPopupMenu();
  
          autoZoom = PROP_DEFAULT_AUTOZOOM.get();
          autoZoomPopup = new JCheckBoxMenuItem();
          autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  autoZoom = !autoZoom;
              }
          });
          autoZoomPopup.setSelected(autoZoom);
          tileOptionMenu.add(autoZoomPopup);
  
          autoLoad = PROP_DEFAULT_AUTOLOAD.get();
          autoLoadPopup = new JCheckBoxMenuItem();
          autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  autoLoad= !autoLoad;
              }
          });
          autoLoadPopup.setSelected(autoLoad);
          tileOptionMenu.add(autoLoadPopup);
  
          showErrors = PROP_DEFAULT_SHOWERRORS.get();
          showErrorsPopup = new JCheckBoxMenuItem();
          showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  showErrors = !showErrors;
              }
          });
          showErrorsPopup.setSelected(showErrors);
          tileOptionMenu.add(showErrorsPopup);
  
          tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  if (clickedTile != null) {
                      loadTile(clickedTile, true);
                      redraw();
                  }
              }
          }));
  
          tileOptionMenu.add(new JMenuItem(new AbstractAction(
                  tr("Show Tile Info")) {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  if (clickedTile != null) {
                      showMetadataTile = clickedTile;
                      redraw();
                  }
              }
          }));
  
          /* FIXME
          tileOptionMenu.add(new JMenuItem(new AbstractAction(
                  tr("Request Update")) {
              public void actionPerformed(ActionEvent ae) {
                  if (clickedTile != null) {
                      clickedTile.requestUpdate();
                      redraw();
                  }
              }
          }));*/
  
          tileOptionMenu.add(new JMenuItem(new AbstractAction(
                  tr("Load All Tiles")) {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  loadAllTiles(true);
                  redraw();
              }
          }));
  
          tileOptionMenu.add(new JMenuItem(new AbstractAction(
                  tr("Load All Error Tiles")) {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  loadAllErrorTiles(true);
                  redraw();
              }
          }));
  
          // increase and decrease commands
          tileOptionMenu.add(new JMenuItem(new AbstractAction(
                  tr("Increase zoom")) {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  increaseZoomLevel();
                  redraw();
              }
          }));
  
          tileOptionMenu.add(new JMenuItem(new AbstractAction(
                  tr("Decrease zoom")) {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  decreaseZoomLevel();
                  redraw();
              }
          }));
  
          tileOptionMenu.add(new JMenuItem(new AbstractAction(
                  tr("Snap to tile size")) {
              @Override
              public void actionPerformed(ActionEvent ae) {
                  double new_factor = Math.sqrt(getScaleFactor(currentZoomLevel));
                  Main.map.mapView.zoomToFactor(new_factor);
                  redraw();
              }
          }));
  
  