@Override
  public void actionPerformed(ActionEvent e) {
    // Send tiles to be rendered
    if ((!tilesWaitingToBeRendered.isEmpty())
        && ((System.currentTimeMillis() - lastTileChange) > 250)) {
      tilesWaitingToBeRendered.forEach(threeDeeRenderManager::renderTile);
      tilesWaitingToBeRendered.clear();
    }

    // Collect rendered tiles
    Set<RenderResult> renderResults = threeDeeRenderManager.getRenderedTiles();
    Rectangle repaintArea = null;
    for (RenderResult renderResult : renderResults) {
      Tile tile = renderResult.getTile();
      int x = tile.getX(), y = tile.getY();
      renderedTiles.put(tile, renderResult.getImage());
      Rectangle tileBounds = zoom(getTileBounds(x, y));
      if (repaintArea == null) {
        repaintArea = tileBounds;
      } else {
        repaintArea = repaintArea.union(tileBounds);
      }
    }
    if (repaintArea != null) {
      //            System.out.println("Repainting " + repaintArea);
      repaint(repaintArea);
    }
  }
  public BufferedImage getImage(ProgressReceiver progressReceiver)
      throws ProgressReceiver.OperationCancelled {
    Tile3DRenderer renderer =
        new Tile3DRenderer(dimension, colourScheme, biomeScheme, customBiomeManager, rotation);

    // Paint the complete image
    java.awt.Dimension preferredSize = unzoom(getPreferredSize());
    BufferedImage image =
        new BufferedImage(preferredSize.width, preferredSize.height, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = image.createGraphics();
    try {
      int tileCount = zSortedTiles.size(), tileNo = 0;
      for (Tile tile : zSortedTiles) {
        Rectangle tileBounds = getTileBounds(tile.getX(), tile.getY());
        g2.drawImage(renderer.render(tile), tileBounds.x, tileBounds.y, null);
        if (progressReceiver != null) {
          tileNo++;
          progressReceiver.setProgress((float) tileNo / tileCount);
        }
      }
    } finally {
      g2.dispose();
    }
    return image;
  }
 @Override
 public void tilesAdded(Dimension dimension, Set<Tile> tiles) {
   //        threeDeeRenderManager.renderTile(tile);
   zSortedTiles.addAll(tiles);
   for (Tile tile : tiles) {
     tile.addListener(this);
   }
 }
 private void scheduleTileForRendering(final Tile tile) {
   //        System.out.println("Scheduling tile for rendering: " + tile.getX() + ", " +
   // tile.getY());
   if (SwingUtilities.isEventDispatchThread()) {
     Rectangle visibleArea = ((JViewport) getParent()).getViewRect();
     Rectangle tileBounds = zoom(getTileBounds(tile.getX(), tile.getY()));
     if (tileBounds.intersects(visibleArea)) {
       // The tile is (partially) visible, so it should be repainted
       // immediately
       switch (refreshMode) {
         case IMMEDIATE:
           threeDeeRenderManager.renderTile(tile);
           break;
         case DELAYED:
           tilesWaitingToBeRendered.add(tile);
           lastTileChange = System.currentTimeMillis();
           break;
         case MANUAL:
           // Do nothing
           break;
         default:
           throw new InternalError();
       }
     } else {
       // The tile is not visible, so repaint it when it becomes visible
       tilesWaitingToBeRendered.remove(tile);
       renderedTiles.remove(tile);
     }
   } else {
     SwingUtilities.invokeLater(
         () -> {
           Rectangle visibleArea = ((JViewport) getParent()).getViewRect();
           Rectangle tileBounds = zoom(getTileBounds(tile.getX(), tile.getY()));
           if (tileBounds.intersects(visibleArea)) {
             // The tile is (partially) visible, so it should be repainted
             // immediately
             switch (refreshMode) {
               case IMMEDIATE:
                 threeDeeRenderManager.renderTile(tile);
                 break;
               case DELAYED:
                 tilesWaitingToBeRendered.add(tile);
                 lastTileChange = System.currentTimeMillis();
                 break;
               case MANUAL:
                 // Do nothing
                 break;
               default:
                 throw new InternalError();
             }
           } else {
             // The tile is not visible, so repaint it when it becomes visible
             tilesWaitingToBeRendered.remove(tile);
             renderedTiles.remove(tile);
           }
         });
   }
 }
 @Override
 public void tilesRemoved(Dimension dimension, Set<Tile> tiles) {
   for (Tile tile : tiles) {
     tile.removeListener(this);
   }
   zSortedTiles.removeAll(tiles);
   //        renderedTiles.remove(new Point(tile.getX(), tile.getY()));
   // TODO: the tile will be re-added if it was on the render queue, but
   // since this can currently never happen anyway we will deal with that
   // when it becomes necessary
 }
 @Override
 public void hierarchyChanged(HierarchyEvent event) {
   if ((event.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0) {
     if (isDisplayable()) {
       //                for (Tile tile: dimension.getTiles()) {
       //                    threeDeeRenderManager.renderTile(tile);
       //                }
       timer = new Timer(250, this);
       timer.start();
     } else {
       timer.stop();
       timer = null;
       threeDeeRenderManager.stop();
       for (Tile tile : dimension.getTiles()) {
         tile.removeListener(this);
       }
       dimension.removeDimensionListener(this);
     }
   }
 }
 @Override
 protected void paintComponent(Graphics g) {
   //        System.out.println("Drawing");
   Graphics2D g2 = (Graphics2D) g;
   if (zoom != 1) {
     double scaleFactor = Math.pow(2.0, zoom - 1);
     //            System.out.println("Scaling with factor " + scaleFactor);
     g2.scale(scaleFactor, scaleFactor);
     if (zoom > 1) {
       g2.setRenderingHint(
           RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
     } else {
       g2.setRenderingHint(
           RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
     }
   }
   if (upsideDown) {
     g2.scale(1.0, -1.0);
     g2.translate(0, -getHeight());
   }
   Rectangle visibleRect = unzoom(getVisibleRect());
   //        System.out.println("Unzoomed visible rectangle: " + visibleRect);
   int centerX = visibleRect.x + visibleRect.width / 2;
   int centerY = visibleRect.y + visibleRect.height / 2 + waterLevel;
   Tile mostCentredTile = null;
   int smallestDistance = Integer.MAX_VALUE;
   Rectangle clipBounds = g.getClipBounds();
   for (Tile tile : zSortedTiles) {
     Rectangle tileBounds = getTileBounds(tile.getX(), tile.getY());
     //            System.out.print("Tile bounds: " + tileBounds);
     if (tileBounds.intersects(clipBounds)) {
       //                System.out.println(" intersects");
       int dx = tileBounds.x + tileBounds.width / 2 - centerX;
       int dy = tileBounds.y + tileBounds.height - TILE_SIZE / 2 - centerY;
       int dist = (int) Math.sqrt((dx * dx) + (dy * dy));
       if (dist < smallestDistance) {
         smallestDistance = dist;
         mostCentredTile = tile;
       }
       BufferedImage tileImg = renderedTiles.get(tile);
       if (tileImg != null) {
         g.drawImage(tileImg, tileBounds.x, tileBounds.y, null);
       } else {
         tilesWaitingToBeRendered.add(0, tile);
       }
       //            } else {
       //                System.out.println(" does NOT intersect");
     }
   }
   if (mostCentredTile != null) {
     centreTile = new Point(mostCentredTile.getX(), mostCentredTile.getY());
   }
   if (highlightTile != null) {
     g.setColor(Color.RED);
     Rectangle rect = getTileBounds(highlightTile.x, highlightTile.y);
     g.drawRect(rect.x, rect.y, rect.width, rect.height);
   }
   if (highlightPoint != null) {
     g.setColor(Color.RED);
     g.drawLine(highlightPoint.x - 2, highlightPoint.y, highlightPoint.x + 2, highlightPoint.y);
     g.drawLine(highlightPoint.x, highlightPoint.y - 2, highlightPoint.x, highlightPoint.y + 2);
   }
   //        for (Map.Entry<Point, BufferedImage> entry: renderedTiles.entrySet()) {
   //            Point tileCoords = entry.getKey();
   //            BufferedImage tileImg = entry.getValue();
   //            Rectangle tileBounds = getTileBounds(tileCoords.x, tileCoords.y);
   //            if (tileBounds.intersects(clipBounds)) {
   //                g.drawImage(tileImg, tileBounds.x, tileBounds.y, null);
   ////                g.setColor(Color.RED);
   ////                g.drawRect(tileBounds.x, tileBounds.y, tileBounds.width, tileBounds.height);
   //            }
   //        }
 }
  public ThreeDeeView(
      Dimension dimension,
      ColourScheme colourScheme,
      BiomeScheme biomeScheme,
      CustomBiomeManager customBiomeManager,
      int rotation,
      int zoom) {
    this.dimension = dimension;
    this.colourScheme = colourScheme;
    this.biomeScheme = biomeScheme;
    this.customBiomeManager = customBiomeManager;
    this.rotation = rotation;
    this.zoom = zoom;
    scale = (int) Math.pow(2.0, Math.abs(zoom - 1));
    //        System.out.println("Zoom " + zoom + " -> scale " + scale);
    maxHeight = dimension.getMaxHeight();
    if (dimension.getTileFactory() instanceof HeightMapTileFactory) {
      waterLevel = ((HeightMapTileFactory) dimension.getTileFactory()).getWaterHeight();
    } else {
      waterLevel = maxHeight / 2;
    }
    upsideDown = dimension.getDim() < 0; // Ceiling dimension
    switch (rotation) {
      case 0:
        zSortedTiles =
            new TreeSet<>(
                (t1, t2) -> {
                  if (t1.getY() != t2.getY()) {
                    return t1.getY() - t2.getY();
                  } else {
                    return t1.getX() - t2.getX();
                  }
                });
        break;
      case 1:
        zSortedTiles =
            new TreeSet<>(
                (t1, t2) -> {
                  if (t1.getX() != t2.getX()) {
                    return t1.getX() - t2.getX();
                  } else {
                    return t2.getY() - t1.getY();
                  }
                });
        break;
      case 2:
        zSortedTiles =
            new TreeSet<>(
                (t1, t2) -> {
                  if (t1.getY() != t2.getY()) {
                    return t2.getY() - t1.getY();
                  } else {
                    return t2.getX() - t1.getX();
                  }
                });
        break;
      case 3:
        zSortedTiles =
            new TreeSet<>(
                (t1, t2) -> {
                  if (t1.getX() != t2.getX()) {
                    return t2.getX() - t1.getX();
                  } else {
                    return t1.getY() - t2.getY();
                  }
                });
        break;
      default:
        throw new IllegalArgumentException();
    }
    zSortedTiles.addAll(dimension.getTiles());
    threeDeeRenderManager =
        new ThreeDeeRenderManager(
            dimension, colourScheme, biomeScheme, customBiomeManager, rotation);

    dimension.addDimensionListener(this);
    for (Tile tile : dimension.getTiles()) {
      tile.addListener(this);
    }

    int width = dimension.getWidth() * TILE_SIZE + dimension.getHeight() * TILE_SIZE;
    int height = width / 2 + maxHeight - 1;
    //        maxX = dimension.getHighestX();
    //        maxY = dimension.getHighestY();
    maxX = maxY = 0;
    //        xOffset = 512;
    //        yOffset = 256;
    //        xOffset = yOffset = 0;
    switch (rotation) {
      case 0:
        xOffset = -getTileBounds(dimension.getLowestX(), dimension.getHighestY()).x;
        yOffset = -getTileBounds(dimension.getLowestX(), dimension.getLowestY()).y;
        break;
      case 1:
        xOffset = -getTileBounds(dimension.getHighestX(), dimension.getHighestY()).x;
        yOffset = -getTileBounds(dimension.getLowestX(), dimension.getHighestY()).y;
        break;
      case 2:
        xOffset = -getTileBounds(dimension.getHighestX(), dimension.getLowestY()).x;
        yOffset = -getTileBounds(dimension.getHighestX(), dimension.getHighestY()).y;
        break;
      case 3:
        xOffset = -getTileBounds(dimension.getLowestX(), dimension.getLowestY()).x;
        yOffset = -getTileBounds(dimension.getHighestX(), dimension.getLowestY()).y;
        break;
      default:
        throw new IllegalArgumentException();
    }
    //        System.out.println("xOffset: " + xOffset + ", yOffset: " + yOffset);
    java.awt.Dimension preferredSize = zoom(new java.awt.Dimension(width, height));
    setPreferredSize(preferredSize);
    setMinimumSize(preferredSize);
    setMaximumSize(preferredSize);
    setSize(preferredSize);

    addHierarchyListener(this);
  }