private void fireTilesChangedIncludeBorder(Set<Tile> tiles) {
   if (showBorder
       && (tileProvider instanceof Dimension)
       && (((Dimension) tileProvider).getDim() == DIM_NORMAL)
       && (((Dimension) tileProvider).getBorder() != null)) {
     final Set<Point> coordSet = new HashSet<>();
     for (Tile tile : tiles) {
       final int tileX = tile.getX(),
           tileY = tile.getY(),
           borderSize = ((Dimension) tileProvider).getBorderSize();
       for (int dx = -borderSize; dx <= borderSize; dx++) {
         for (int dy = -borderSize; dy <= borderSize; dy++) {
           coordSet.add(getTileCoordinates(tileX + dx, tileY + dy));
         }
       }
     }
     for (TileListener listener : listeners) {
       listener.tilesChanged(this, coordSet);
     }
   } else {
     Set<Point> coords = tiles.stream().map(this::getTileCoordinates).collect(Collectors.toSet());
     for (TileListener listener : listeners) {
       listener.tilesChanged(this, coords);
     }
   }
 }
 @Override
 public void tilesRemoved(Dimension dimension, Set<Tile> tiles) {
   for (Tile tile : tiles) {
     tile.removeListener(this);
   }
   fireTilesChangedIncludeBorder(tiles);
 }
 @Override
 public void removeTileListener(TileListener tileListener) {
   listeners.remove(tileListener);
   if (active && listeners.isEmpty()) {
     for (Tile tile : ((Dimension) tileProvider).getTiles()) {
       tile.removeListener(this);
     }
     ((Dimension) tileProvider).removeDimensionListener(this);
   }
 }
 @Override
 public void addTileListener(TileListener tileListener) {
   if (active && listeners.isEmpty()) {
     ((Dimension) tileProvider).addDimensionListener(this);
     for (Tile tile : ((Dimension) tileProvider).getTiles()) {
       tile.addListener(this);
     }
   }
   if (!listeners.contains(tileListener)) {
     listeners.add(tileListener);
   }
 }
 /**
  * Convert the actual tile coordinates to zoom-corrected (tile provider coordinate system)
  * coordinates.
  *
  * @param tile The tile of which to convert the coordinates.
  * @return The coordinates of the tile in the tile provider coordinate system (corrected for
  *     zoom).
  */
 private Point getTileCoordinates(Tile tile) {
   return getTileCoordinates(tile.getX(), tile.getY());
 }
  private void paintUnzoomedTile(
      final Image tileImage, final int x, final int y, final int dx, final int dy) {
    TileType tileType = getUnzoomedTileType(x, y);
    switch (tileType) {
      case WORLD:
        Tile tile = tileProvider.getTile(x, y);
        if (tile.hasLayer(NotPresent.INSTANCE) && (surroundingTileProvider != null)) {
          surroundingTileProvider.paintTile(tileImage, x, y, dx, dy);
        }
        TileRenderer tileRenderer = tileRendererRef.get();
        tileRenderer.setTile(tile);
        tileRenderer.renderTile(tileImage, dx, dy);
        break;
      case BORDER:
        int colour;
        switch (((Dimension) tileProvider).getBorder()) {
          case WATER:
            colour = colourScheme.getColour(BLK_WATER);
            break;
          case LAVA:
            colour = colourScheme.getColour(BLK_LAVA);
            break;
          case VOID:
            colour = VoidRenderer.getColour();
            break;
          default:
            throw new InternalError();
        }
        Graphics2D g2 = (Graphics2D) tileImage.getGraphics();
        try {
          g2.setColor(new Color(colour));
          g2.fillRect(dx, dy, TILE_SIZE, TILE_SIZE);

          // Draw border lines
          g2.setColor(Color.BLACK);
          g2.setStroke(
              new BasicStroke(
                  2,
                  BasicStroke.CAP_BUTT,
                  BasicStroke.JOIN_BEVEL,
                  0.0f,
                  new float[] {4.0f, 4.0f},
                  0.0f));
          if (tileProvider.isTilePresent(x, y - 1)) {
            g2.drawLine(dx + 1, dy + 1, dx + TILE_SIZE - 1, dy + 1);
          }
          if (tileProvider.isTilePresent(x + 1, y)) {
            g2.drawLine(dx + TILE_SIZE - 1, dy + 1, dx + TILE_SIZE - 1, dy + TILE_SIZE - 1);
          }
          if (tileProvider.isTilePresent(x, y + 1)) {
            g2.drawLine(dx + 1, dy + TILE_SIZE - 1, dx + TILE_SIZE - 1, dy + TILE_SIZE - 1);
          }
          if (tileProvider.isTilePresent(x - 1, y)) {
            g2.drawLine(dx + 1, dy + 1, dx + 1, dy + TILE_SIZE - 1);
          }
        } finally {
          g2.dispose();
        }
        break;
      case WALL:
        if (surroundingTileProvider != null) {
          surroundingTileProvider.paintTile(tileImage, x, y, dx, dy);
        }
        g2 = (Graphics2D) tileImage.getGraphics();
        try {
          if (surroundingTileProvider == null) {
            // A surrounding tile provider would have completely
            // filled the image, but since there isn't one we have
            // to make sure of that ourselves
            g2.setColor(new Color(VoidRenderer.getColour()));
            g2.fillRect(dx, dy, TILE_SIZE, TILE_SIZE);
          }
          g2.setColor(new Color(colourScheme.getColour(BLK_BEDROCK)));
          TileType neighbourType = getUnzoomedTileType(x, y - 1);
          if ((neighbourType == TileType.WORLD) || (neighbourType == TileType.BORDER)) {
            g2.fillRect(dx, dy, TILE_SIZE, 16);
          }
          neighbourType = getUnzoomedTileType(x + 1, y);
          if ((neighbourType == TileType.WORLD) || (neighbourType == TileType.BORDER)) {
            g2.fillRect(dx + TILE_SIZE - 16, dy, 16, TILE_SIZE);
          }
          neighbourType = getUnzoomedTileType(x, y + 1);
          if ((neighbourType == TileType.WORLD) || (neighbourType == TileType.BORDER)) {
            g2.fillRect(dx, dy + TILE_SIZE - 16, TILE_SIZE, 16);
          }
          neighbourType = getUnzoomedTileType(x - 1, y);
          if ((neighbourType == TileType.WORLD) || (neighbourType == TileType.BORDER)) {
            g2.fillRect(dx, dy, 16, TILE_SIZE);
          }
        } finally {
          g2.dispose();
        }
        break;
      case SURROUNDS:
        if (surroundingTileProvider != null) {
          surroundingTileProvider.paintTile(tileImage, x, y, dx, dy);
        }
        break;
      default:
        throw new InternalError();
    }
  }
  @Override
  public void paintTile(
      final Image tileImage, final int x, final int y, final int imageX, final int imageY) {
    try {
      if (zoom == 0) {
        paintUnzoomedTile(tileImage, x, y, imageX, imageY);
      } else {
        Graphics2D g2 = (Graphics2D) tileImage.getGraphics();
        try {
          BufferedImage surroundingTileImage = null;
          final Color waterColour = new Color(colourScheme.getColour(BLK_WATER));
          final Color lavaColour = new Color(colourScheme.getColour(BLK_LAVA));
          final Color voidColour = new Color(VoidRenderer.getColour());
          final Color bedrockColour = new Color(colourScheme.getColour(BLK_BEDROCK));
          final int scale = 1 << -zoom;
          final int subSize = TILE_SIZE / scale;
          for (int dx = 0; dx < scale; dx++) {
            for (int dy = 0; dy < scale; dy++) {
              TileType tileType = getUnzoomedTileType(x * scale + dx, y * scale + dy);
              switch (tileType) {
                case WORLD:
                  Tile tile = tileProvider.getTile(x * scale + dx, y * scale + dy);
                  if (tile.hasLayer(NotPresent.INSTANCE)) {
                    if (surroundingTileProvider != null) {
                      if (surroundingTileImage == null) {
                        surroundingTileImage =
                            new BufferedImage(TILE_SIZE, TILE_SIZE, BufferedImage.TYPE_INT_ARGB);
                        surroundingTileProvider.paintTile(surroundingTileImage, x, y, 0, 0);
                      }
                      g2.drawImage(
                          surroundingTileImage,
                          imageX + dx * subSize,
                          imageY + dy * subSize,
                          imageX + (dx + 1) * subSize,
                          imageY + (dy + 1) * subSize,
                          imageX + dx * subSize,
                          imageY + dy * subSize,
                          imageX + (dx + 1) * subSize,
                          imageY + (dy + 1) * subSize,
                          null);
                    } else {
                      g2.setColor(voidColour);
                      g2.fillRect(imageX + dx * subSize, imageY + dy * subSize, subSize, subSize);
                    }
                  }
                  TileRenderer tileRenderer = tileRendererRef.get();
                  tileRenderer.setTile(tile);
                  tileRenderer.renderTile(tileImage, dx * subSize, dy * subSize);
                  break;
                case BORDER:
                  Color colour;
                  switch (((Dimension) tileProvider).getBorder()) {
                    case WATER:
                      colour = waterColour;
                      break;
                    case LAVA:
                      colour = lavaColour;
                      break;
                    case VOID:
                      colour = voidColour;
                      break;
                    default:
                      throw new InternalError();
                  }
                  g2.setColor(colour);
                  g2.fillRect(imageX + dx * subSize, imageY + dy * subSize, subSize, subSize);

                  // Draw border lines
                  g2.setColor(Color.BLACK);
                  g2.setStroke(
                      new BasicStroke(
                          2,
                          BasicStroke.CAP_BUTT,
                          BasicStroke.JOIN_BEVEL,
                          0.0f,
                          new float[] {4.0f, 4.0f},
                          0.0f));
                  if (tileProvider.isTilePresent(x * scale + dx, y * scale + dy - 1)) {
                    g2.drawLine(
                        imageX + dx * subSize,
                        imageY + dy * subSize,
                        imageX + (dx + 1) * subSize - 1,
                        imageY + dy * subSize);
                  }
                  if (tileProvider.isTilePresent(x * scale + dx + 1, y * scale + dy)) {
                    g2.drawLine(
                        imageX + (dx + 1) * subSize - 1,
                        imageY + dy * subSize,
                        imageX + (dx + 1) * subSize - 1,
                        imageY + (dy + 1) * subSize - 1);
                  }
                  if (tileProvider.isTilePresent(x * scale + dx, y * scale + dy + 1)) {
                    g2.drawLine(
                        imageX + dx * subSize,
                        imageY + (dy + 1) * subSize - 1,
                        imageX + (dx + 1) * subSize - 1,
                        imageY + (dy + 1) * subSize - 1);
                  }
                  if (tileProvider.isTilePresent(x * scale + dx - 1, y * scale + dy)) {
                    g2.drawLine(
                        imageX + dx * subSize,
                        imageY + dy * subSize,
                        imageX + dx * subSize,
                        imageY + (dy + 1) * subSize - 1);
                  }
                  break;
                case SURROUNDS:
                case WALL:
                  if (surroundingTileProvider != null) {
                    if (surroundingTileImage == null) {
                      surroundingTileImage =
                          new BufferedImage(TILE_SIZE, TILE_SIZE, BufferedImage.TYPE_INT_ARGB);
                      surroundingTileProvider.paintTile(surroundingTileImage, x, y, 0, 0);
                    }
                    g2.drawImage(
                        surroundingTileImage,
                        imageX + dx * subSize,
                        imageY + dy * subSize,
                        imageX + (dx + 1) * subSize,
                        imageY + (dy + 1) * subSize,
                        imageX + dx * subSize,
                        imageY + dy * subSize,
                        imageX + (dx + 1) * subSize,
                        imageY + (dy + 1) * subSize,
                        null);
                  } else {
                    g2.setColor(voidColour);
                    g2.fillRect(imageX + dx * subSize, imageY + dy * subSize, subSize, subSize);
                  }
                  if (tileType == TileType.WALL) {
                    g2.setColor(bedrockColour);
                    TileType neighbourType =
                        getUnzoomedTileType(x * scale + dx, y * scale + dy - 1);
                    int wallWidth = Math.max(subSize / 8, 1);
                    if ((neighbourType == TileType.WORLD) || (neighbourType == TileType.BORDER)) {
                      g2.fillRect(imageX + dx * subSize, imageY + dy * subSize, subSize, wallWidth);
                    }
                    neighbourType = getUnzoomedTileType(x * scale + dx + 1, y * scale + dy);
                    if ((neighbourType == TileType.WORLD) || (neighbourType == TileType.BORDER)) {
                      g2.fillRect(
                          imageX + (dx + 1) * subSize - wallWidth,
                          imageY + dy * subSize,
                          wallWidth,
                          subSize);
                    }
                    neighbourType = getUnzoomedTileType(x * scale + dx, y * scale + dy + 1);
                    if ((neighbourType == TileType.WORLD) || (neighbourType == TileType.BORDER)) {
                      g2.fillRect(
                          imageX + dx * subSize,
                          imageY + (dy + 1) * subSize - wallWidth,
                          subSize,
                          wallWidth);
                    }
                    neighbourType = getUnzoomedTileType(x * scale + dx - 1, y * scale + dy);
                    if ((neighbourType == TileType.WORLD) || (neighbourType == TileType.BORDER)) {
                      g2.fillRect(imageX + dx * subSize, imageY + dy * subSize, wallWidth, subSize);
                    }
                  }
                  break;
              }
            }
          }
        } finally {
          g2.dispose();
        }
      }
    } catch (Throwable e) {
      // Log at debug level because this tends to happen when zooming in
      // and out, probably due to some state getting out of sync. It
      // doesn't so far appear to have any visible consequences.
      logger.error("Exception while generating image for tile at " + x + ", " + y, e);
    }
  }