content: // License: GPL. For details, see Readme.txt file.
  package amodeus.amodeus.view.jmapviewer;
  
  import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.Graphics;
  import java.awt.Point;
  import java.util.ArrayList;
  import java.util.List;
  
  import javax.swing.JComponent;
  
  import amodeus.amodeus.view.jmapviewer.interfaces.ICoordinate;
  import amodeus.amodeus.view.jmapviewer.interfaces.TileCache;
  import amodeus.amodeus.view.jmapviewer.interfaces.TileLoaderListener;
  import amodeus.amodeus.view.jmapviewer.interfaces.TileSource;
  import amodeus.amodeus.view.jmapviewer.tilesources.MapnikTileSource;
  
  /** Provides a simple panel that displays pre-rendered map tiles loaded from the
   * OpenStreetMap project.
   *
   * @author Jan Peter Stotz
   * @author Jason Huntley */
  public class JMapViewer extends JComponent implements TileLoaderListener {
      /** Vectors for clock-wise tile painting */
      private static final Point[] MOVE = { new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1) };
  
      /** Maximum zoom level */
      public static final int MAX_ZOOM = 22;
      /** Minimum zoom level */
      public static final int MIN_ZOOM = 0;
  
      /** whether debug mode is enabled or not */
      public static boolean DEBUG;
  
      /** option to reverse zoom direction with mouse wheel */
      public static boolean ZOOMREVERSEWHEEL;
      // ---
      protected boolean tileGridVisible = false; // <- for debug purposes
      protected boolean scrollWrapEnabled;
  
      private transient TileController tileController;
  
      protected final List<AmodeusHeatMap> matsimHeatmaps = new ArrayList<>();
  
      /** x- and y-position of the center of this map-panel on the world map
       * denoted in screen pixel regarding the current zoom level. */
      protected Point center;
  
      /** Current zoom level */
      protected int zoom;
  
      public int mapAlphaCover = 192;
      public int mapGrayCover = 0;
  
      private transient TileSource tileSource;
  
      private transient AttributionSupport attribution = new AttributionSupport();
  
      /** Creates a standard {@link JMapViewer} instance that can be controlled via
       * mouse: hold right mouse button for moving, double click left mouse button
       * or use mouse wheel for zooming. Loaded tiles are stored in a
       * {@link MemoryTileCache} and the tile loader uses 4 parallel threads for
       * retrieving the tiles. */
      public JMapViewer() {
          this(new MemoryTileCache());
          new DefaultMapController(this);
      }
  
      /** Creates a new {@link JMapViewer} instance.
       * 
       * @param tileCache
       *            The cache where to store tiles */
      JMapViewer(TileCache tileCache) {
          tileSource = MapnikTileSource.INSTANCE;
          tileController = new TileController(tileSource, tileCache, this);
          setLayout(null);
          setMinimumSize(new Dimension(tileSource.getTileSize(), tileSource.getTileSize()));
          setPreferredSize(new Dimension(400, 400));
          setDisplayPosition(new Coordinate(50, 9), 3);
      }
  
      /** Changes the map pane so that it is centered on the specified coordinate
       * at the given zoom level.
       *
       * @param to
       *            specified coordinate
       * @param zoom
       *            {@link #MIN_ZOOM} &lt;= zoom level &lt;= {@link #MAX_ZOOM} */
      public void setDisplayPosition(ICoordinate to, int zoom) {
          setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), to, zoom);
      }
  
      /** Changes the map pane so that the specified coordinate at the given zoom
       * level is displayed on the map at the screen coordinate
       * <code>mapPoint</code>.
       *
       * @param mapPoint
       *            point on the map denoted in pixels where the coordinate should
       *            be set
       * @param to
       *            specified coordinate
       * @param zoom
       *            {@link #MIN_ZOOM} &lt;= zoom level &lt;=
       *            {@link TileSource#getMaxZoom()} */
      public void setDisplayPosition(Point mapPoint, ICoordinate to, int zoom) {
          Point p = tileSource.latLonToXY(to, zoom);
          setDisplayPosition(mapPoint, p.x, p.y, zoom);
      }
  
      /** Sets the display position.
       * 
       * @param x
       *            X coordinate
       * @param y
       *            Y coordinate
       * @param zoom
       *            zoom level, between {@link #MIN_ZOOM} and {@link #MAX_ZOOM} */
      void setDisplayPosition(int x, int y, int zoom) {
          setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), x, y, zoom);
      }
  
      /** Sets the display position.
       * 
       * @param mapPoint
       *            map point
       * @param x
       *            X coordinate
       * @param y
       *            Y coordinate
       * @param zoom
       *            zoom level, between {@link #MIN_ZOOM} and {@link #MAX_ZOOM} */
      void setDisplayPosition(Point mapPoint, int x, int y, int zoom) {
          if (zoom > tileController.getTileSource().getMaxZoom() || zoom < MIN_ZOOM)
              return;
  
          // Get the plain tile number
          Point p = new Point();
          p.x = x - mapPoint.x + getWidth() / 2;
          p.y = y - mapPoint.y + getHeight() / 2;
          center = p;
          this.zoom = zoom;
          repaint();
      }
  
      /** Calculates the latitude/longitude coordinate of the center of the
       * currently displayed map area.
       *
       * @return latitude / longitude */
      public ICoordinate getPosition() {
          return tileSource.xyToLatLon(center, zoom);
      }
  
      /** Converts the relative pixel coordinate (regarding the top left corner of
       * the displayed map) into a latitude / longitude coordinate
       *
       * @param mapPoint
       *            relative pixel coordinate regarding the top left corner of the
       *            displayed map
       * @return latitude / longitude */
      public ICoordinate getPosition(Point mapPoint) {
          return getPosition(mapPoint.x, mapPoint.y);
      }
  
      /** Converts the relative pixel coordinate (regarding the top left corner of
       * the displayed map) into a latitude / longitude coordinate
       *
       * @param mapPointX
       *            X coordinate
       * @param mapPointY
       *            Y coordinate
       * @return latitude / longitude */
      private ICoordinate getPosition(int mapPointX, int mapPointY) {
          int x = center.x + mapPointX - getWidth() / 2;
          int y = center.y + mapPointY - getHeight() / 2;
          return tileSource.xyToLatLon(x, y, zoom);
      }
  
      /** Calculates the position on the map of a given coordinate
       *
       * @param lat
       *            latitude
       * @param lon
       *            longitude
       * @param checkOutside
       *            check if the point is outside the displayed area
       * @return point on the map or <code>null</code> if the point is not visible
       *         and checkOutside set to <code>true</code> */
      public Point getMapPosition(double lat, double lon, boolean checkOutside) {
          Point p = tileSource.latLonToXY(lat, lon, zoom);
          p.translate(-(center.x - getWidth() / 2), -(center.y - getHeight() / 2));
  
          if (checkOutside && (p.x < 0 || p.y < 0 || p.x > getWidth() || p.y > getHeight())) {
              return null;
          }
          return p;
      }
  
      /** Calculates the position on the map of a given coordinate
       *
       * @param lat
       *            latitude
       * @param lon
       *            longitude
       * @return point on the map or <code>null</code> if the point is not visible */
      public Point getMapPosition(double lat, double lon) {
          return getMapPosition(lat, lon, true);
      }
  
      /** Gets the meter per pixel.
       *
       * @return the meter per pixel */
      public double getMeterPerPixel() {
          Point origin = new Point(5, 5);
          Point center = new Point(getWidth() / 2, getHeight() / 2);
  
          double pDistance = center.distance(origin);
  
          ICoordinate originCoord = getPosition(origin);
          ICoordinate centerCoord = getPosition(center);
  
          double mDistance = tileSource.getDistance(originCoord.getLat(), originCoord.getLon(), centerCoord.getLat(), centerCoord.getLon());
  
          return mDistance / pDistance;
      }
  
      @Override
      protected void paintComponent(Graphics g) {
          // super.paintComponent(g);
  
          int iMove = 0;
  
          int tilesize = tileSource.getTileSize();
          int tilex = center.x / tilesize;
          int tiley = center.y / tilesize;
          int offsx = center.x % tilesize;
          int offsy = center.y % tilesize;
  
          int w2 = getWidth() / 2;
          int h2 = getHeight() / 2;
          int posx = w2 - offsx;
          int posy = h2 - offsy;
  
          int diffLeft = offsx;
          int diffRight = tilesize - offsx;
          int diffTop = offsy;
          int diffBottom = tilesize - offsy;
  
          boolean startLeft = diffLeft < diffRight;
          boolean startTop = diffTop < diffBottom;
  
          if (startTop) {
              if (startLeft) {
                  iMove = 2;
              } else {
                  iMove = 3;
              }
          } else {
              if (startLeft) {
                  iMove = 1;
              } else {
                  iMove = 0;
              }
          } // calculate the visibility borders
          int xMin = -tilesize;
          int yMin = -tilesize;
          int xMax = getWidth();
          int yMax = getHeight();
  
          // calculate the length of the grid (number of squares per edge)
          int gridLength = 1 << zoom;
  
          // paint the tiles in a spiral, starting from center of the map
          boolean painted = true;
          int x = 0;
          g.setFont(new Font(Font.DIALOG, Font.PLAIN, 10)); // jan added this
          while (painted) {
              painted = false;
              for (int i = 0; i < 4; i++) {
                  if (i % 2 == 0) {
                      x++;
                  }
                  for (int j = 0; j < x; j++) {
                      if (xMin <= posx && posx <= xMax && yMin <= posy && posy <= yMax) {
                          // tile is visible
                          Tile tile;
                          if (isScrollWrapEnabled()) {
                              // in case tilex is out of bounds, grab the tile to use for wrapping
                              int tilexWrap = ((tilex % gridLength) + gridLength) % gridLength;
                              tile = tileController.getTile(tilexWrap, tiley, zoom);
                          } else {
                              tile = tileController.getTile(tilex, tiley, zoom);
                          }
                          if (tile != null) {
                              tile.paint(g, posx, posy); // , tilesize, tilesize
  
                              {
                                  int rgb = mapGrayCover;
                                  g.setColor(new Color(rgb, rgb, rgb, mapAlphaCover));
                                  g.fillRect(posx, posy, 256, 256);
                              }
  
                              for (AmodeusHeatMap matsimHeatmap : matsimHeatmaps)
                                  matsimHeatmap.render(g, tile, zoom, posx, posy);
  
                              if (isTileGridVisible()) {
                                  g.setColor(new Color(0, 0, 0, 32));
                                  g.drawLine(posx + 1, posy, posx + tilesize, posy);
                                  g.drawLine(posx, posy + 1, posx, posy + tilesize);
                                  g.setColor(new Color(0, 0, 0, 128));
                                  g.drawString(String.format("x=%d y=%d", tile.getXtile(), tile.getYtile()), posx, posy + 10);
                              }
                          }
                          painted = true;
                      }
                      Point p = MOVE[iMove];
                      posx += p.x * tilesize;
                      posy += p.y * tilesize;
                      tilex += p.x;
                      tiley += p.y;
                  }
                  iMove = (iMove + 1) % MOVE.length;
              }
          }
          // outer border of the map
          int mapSize = tilesize << zoom;
          if (scrollWrapEnabled) {
              g.drawLine(0, h2 - center.y, getWidth(), h2 - center.y);
              g.drawLine(0, h2 - center.y + mapSize, getWidth(), h2 - center.y + mapSize);
          } else {
              g.drawRect(w2 - center.x, h2 - center.y, mapSize, mapSize);
          }
  
          // g.drawString("Tiles in cache: " + tileCache.getTileCount(), 50, 20);
  
          // keep x-coordinates from growing without bound if scroll-wrap is enabled
          if (scrollWrapEnabled) {
              center.x = center.x % mapSize;
          }
  
          attribution.paintAttribution(g, getWidth(), getHeight(), getPosition(0, 0), getPosition(getWidth(), getHeight()), zoom, this);
  
      }
  
      /** Moves the visible map pane.
       *
       * @param x
       *            horizontal movement in pixel.
       * @param y
       *            vertical movement in pixel */
      public void moveMap(int x, int y) {
          tileController.cancelOutstandingJobs(); // Clear outstanding load
          center.x += x;
          center.y += y;
          repaint();
      }
  
      /** @return the current zoom level */
      public int getZoom() {
          return zoom;
      }
  
      /** Increases the current zoom level by one */
      public void zoomIn() {
          setZoom(zoom + 1);
      }
  
      /** Increases the current zoom level by one
       * 
       * @param mapPoint
       *            point to choose as center for new zoom level */
      public void zoomIn(Point mapPoint) {
          setZoom(zoom + 1, mapPoint);
      }
  
      /** Decreases the current zoom level by one */
      public void zoomOut() {
          setZoom(zoom - 1);
      }
  
      /** Decreases the current zoom level by one
       *
       * @param mapPoint
       *            point to choose as center for new zoom level */
      public void zoomOut(Point mapPoint) {
          setZoom(zoom - 1, mapPoint);
      }
  
      /** Set the zoom level and center point for display
       *
       * @param zoom
       *            new zoom level
       * @param mapPoint
       *            point to choose as center for new zoom level */
      public void setZoom(int zoom, Point mapPoint) {
          if (zoom > tileController.getTileSource().getMaxZoom() || zoom < tileController.getTileSource().getMinZoom() || zoom == this.zoom)
              return;
          ICoordinate zoomPos = getPosition(mapPoint);
          tileController.cancelOutstandingJobs(); // Clearing outstanding load
          // requests
          setDisplayPosition(mapPoint, zoomPos, zoom);
      }
  
      /** Set the zoom level
       *
       * @param zoom
       *            new zoom level */
      public void setZoom(int zoom) {
          setZoom(zoom, new Point(getWidth() / 2, getHeight() / 2));
      }
  
      /** Determines whether the tile grid is visible or not.
       * 
       * @return {@code true} if the tile grid is visible, {@code false} otherwise */
      public boolean isTileGridVisible() {
          return tileGridVisible;
      }
  
      /** Sets whether the tile grid is visible or not.
       * 
       * @param tileGridVisible
       *            {@code true} if the tile grid is visible, {@code false} otherwise */
      public void setTileGridVisible(boolean tileGridVisible) {
          this.tileGridVisible = tileGridVisible;
          repaint();
      }
  
      /** Sets the tile source.
       * 
       * @param tileSource
       *            tile source */
      public void setTileSource(TileSource tileSource) {
          if (tileSource.getMaxZoom() > MAX_ZOOM)
              throw new RuntimeException("Maximum zoom level too high");
          if (tileSource.getMinZoom() < MIN_ZOOM)
              throw new RuntimeException("Minimum zoom level too low");
          ICoordinate position = getPosition();
          this.tileSource = tileSource;
          tileController.setTileSource(tileSource);
          tileController.cancelOutstandingJobs();
          if (zoom > tileSource.getMaxZoom()) {
              setZoom(tileSource.getMaxZoom());
          }
          attribution.initialize(tileSource);
          setDisplayPosition(position, zoom);
          repaint();
      }
  
      public TileSource getTileSource() {
          return tileSource;
      }
  
      @Override
      public void tileLoadingFinished(Tile tile, boolean success) {
          tile.setLoaded(success);
          repaint();
      }
  
      /** Determines whether scroll wrap is enabled or not.
       * 
       * @return {@code true} if scroll wrap is enabled, {@code false} otherwise */
      public boolean isScrollWrapEnabled() {
          return scrollWrapEnabled;
      }
  
      /** Sets whether scroll wrap is enabled or not.
       * 
       * @param scrollWrapEnabled
       *            {@code true} if scroll wrap is enabled, {@code false} otherwise */
      public void setScrollWrapEnabled(boolean scrollWrapEnabled) {
          this.scrollWrapEnabled = scrollWrapEnabled;
          repaint();
      }
  
      /** Returns attribution.
       * 
       * @return attribution */
      public AttributionSupport getAttribution() {
          return attribution;
      }
  }
  