@Override
 public void addClientConfiguration(JSONObject mapObject) {
   s(mapObject, "perspective", name);
   s(mapObject, "azimuth", azimuth);
   s(mapObject, "inclination", inclination);
   s(mapObject, "scale", scale);
   s(mapObject, "worldtomap", world_to_map.toJSON());
   s(mapObject, "maptoworld", map_to_world.toJSON());
   int dir = ((360 + (int) (22.5 + azimuth)) / 45) % 8;
   ;
   s(mapObject, "compassview", directions[dir]);
 }
  public IsoHDPerspective(ConfigurationNode configuration) {
    name = configuration.getString("name", null);
    if (name == null) {
      Log.severe("Perspective definition missing name - must be defined and unique");
      return;
    }
    azimuth =
        configuration.getDouble("azimuth", 135.0); /* Get azimuth (default to classic kzed POV */
    inclination = configuration.getDouble("inclination", 60.0);
    if (inclination > MAX_INCLINATION) inclination = MAX_INCLINATION;
    if (inclination < MIN_INCLINATION) inclination = MIN_INCLINATION;
    scale = configuration.getDouble("scale", MIN_SCALE);
    if (scale < MIN_SCALE) scale = MIN_SCALE;
    if (scale > MAX_SCALE) scale = MAX_SCALE;
    /* Get max and min height */
    maxheight = configuration.getInteger("maximumheight", 127);
    if (maxheight > 127) maxheight = 127;
    minheight = configuration.getInteger("minimumheight", 0);
    if (minheight < 0) minheight = 0;

    /* Generate transform matrix for world-to-tile coordinate mapping */
    /* First, need to fix basic coordinate mismatches before rotation - we want zero azimuth to have north to top
     * (world -X -> tile +Y) and east to right (world -Z to tile +X), with height being up (world +Y -> tile +Z)
     */
    Matrix3D transform = new Matrix3D(0.0, 0.0, -1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 0.0);
    /* Next, rotate world counterclockwise around Z axis by azumuth angle */
    transform.rotateXY(180 - azimuth);
    /* Next, rotate world by (90-inclination) degrees clockwise around +X axis */
    transform.rotateYZ(90.0 - inclination);
    /* Finally, shear along Z axis to normalize Z to be height above map plane */
    transform.shearZ(0, Math.tan(Math.toRadians(90.0 - inclination)));
    /* And scale Z to be same scale as world coordinates, and scale X and Y based on setting */
    transform.scale(scale, scale, Math.sin(Math.toRadians(inclination)));
    world_to_map = transform;
    /* Now, generate map to world tranform, by doing opposite actions in reverse order */
    transform = new Matrix3D();
    transform.scale(1.0 / scale, 1.0 / scale, 1 / Math.sin(Math.toRadians(inclination)));
    transform.shearZ(0, -Math.tan(Math.toRadians(90.0 - inclination)));
    transform.rotateYZ(-(90.0 - inclination));
    transform.rotateXY(-180 + azimuth);
    Matrix3D coordswap = new Matrix3D(0.0, -1.0, 0.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0);
    transform.multiply(coordswap);
    map_to_world = transform;
    /* Scaled models for non-cube blocks */
    modscale = (int) Math.ceil(scale);
    scalemodels = HDBlockModels.getModelsForScale(modscale);
    ;
  }
  @Override
  public MapTile[] getTiles(Location loc) {
    DynmapWorld world = MapManager.mapman.getWorld(loc.getWorld().getName());
    HashSet<MapTile> tiles = new HashSet<MapTile>();
    Vector3D block = new Vector3D();
    block.setFromLocation(loc); /* Get coordinate for block */
    Vector3D corner = new Vector3D();
    /* Loop through corners of the cube */
    for (int i = 0; i < 2; i++) {
      double inity = block.y;
      for (int j = 0; j < 2; j++) {
        double initz = block.z;
        for (int k = 0; k < 2; k++) {
          world_to_map.transform(block, corner); /* Get map coordinate of corner */
          addTile(
              tiles,
              world,
              (int) Math.floor(corner.x / tileWidth),
              (int) Math.floor(corner.y / tileHeight));

          block.z += 1;
        }
        block.z = initz;
        block.y += 1;
      }
      block.y = inity;
      block.x += 1;
    }
    return tiles.toArray(new MapTile[tiles.size()]);
  }
 @Override
 public MapTile[] getTiles(Location loc0, Location loc1) {
   DynmapWorld world = MapManager.mapman.getWorld(loc0.getWorld().getName());
   HashSet<MapTile> tiles = new HashSet<MapTile>();
   Vector3D blocks[] = new Vector3D[] {new Vector3D(), new Vector3D()};
   /* Get ordered point - 0=minX,Y,Z, 1=maxX,Y,Z */
   if (loc0.getBlockX() < loc1.getBlockX()) {
     blocks[0].x = loc0.getBlockX();
     blocks[1].x = loc1.getBlockX() + 1;
   } else {
     blocks[0].x = loc1.getBlockX();
     blocks[1].x = loc0.getBlockX() + 1;
   }
   if (loc0.getBlockY() < loc1.getBlockY()) {
     blocks[0].y = loc0.getBlockY();
     blocks[1].y = loc1.getBlockY() + 1;
   } else {
     blocks[0].y = loc1.getBlockY();
     blocks[1].y = loc0.getBlockY() + 1;
   }
   if (loc0.getBlockZ() < loc1.getBlockZ()) {
     blocks[0].z = loc0.getBlockZ();
     blocks[1].z = loc1.getBlockZ() + 1;
   } else {
     blocks[0].z = loc1.getBlockZ();
     blocks[1].z = loc0.getBlockZ() + 1;
   }
   Vector3D corner = new Vector3D();
   Vector3D tcorner = new Vector3D();
   int mintilex = Integer.MAX_VALUE;
   int maxtilex = Integer.MIN_VALUE;
   int mintiley = Integer.MAX_VALUE;
   int maxtiley = Integer.MIN_VALUE;
   /* Loop through corners of the prism */
   for (int i = 0; i < 2; i++) {
     corner.x = blocks[i].x;
     for (int j = 0; j < 2; j++) {
       corner.y = blocks[j].y;
       for (int k = 0; k < 2; k++) {
         corner.z = blocks[k].z;
         world_to_map.transform(corner, tcorner); /* Get map coordinate of corner */
         int tx = (int) Math.floor(tcorner.x / tileWidth);
         int ty = (int) Math.floor(tcorner.y / tileWidth);
         if (mintilex > tx) mintilex = tx;
         if (maxtilex < tx) maxtilex = tx;
         if (mintiley > ty) mintiley = ty;
         if (maxtiley < ty) maxtiley = ty;
       }
     }
   }
   /* Now, add the tiles for the ranges - not perfect, but it works (some extra tiles on corners possible) */
   for (int i = mintilex; i <= maxtilex; i++) {
     for (int j = mintiley; j < maxtiley; j++) {
       addTile(tiles, world, i, j);
     }
   }
   return tiles.toArray(new MapTile[tiles.size()]);
 }
  @Override
  public boolean render(MapChunkCache cache, HDMapTile tile, String mapname) {
    Color rslt = new Color();
    MapIterator mapiter = cache.getIterator(0, 0, 0);
    /* Build shader state object for each shader */
    HDShaderState[] shaderstate =
        MapManager.mapman.hdmapman.getShaderStateForTile(tile, cache, mapiter, mapname);
    int numshaders = shaderstate.length;
    if (numshaders == 0) return false;
    /* Check if nether world */
    boolean isnether = tile.getWorld().getEnvironment() == Environment.NETHER;
    /* Create buffered image for each */
    DynmapBufferedImage im[] = new DynmapBufferedImage[numshaders];
    DynmapBufferedImage dayim[] = new DynmapBufferedImage[numshaders];
    int[][] argb_buf = new int[numshaders][];
    int[][] day_argb_buf = new int[numshaders][];

    for (int i = 0; i < numshaders; i++) {
      HDShader shader = shaderstate[i].getShader();
      HDLighting lighting = shaderstate[i].getLighting();
      if (shader.isEmittedLightLevelNeeded() || lighting.isEmittedLightLevelNeeded())
        need_emittedlightlevel = true;
      if (shader.isSkyLightLevelNeeded() || lighting.isSkyLightLevelNeeded())
        need_skylightlevel = true;
      im[i] = DynmapBufferedImage.allocateBufferedImage(tileWidth, tileHeight);
      argb_buf[i] = im[i].argb_buf;
      if (lighting.isNightAndDayEnabled()) {
        dayim[i] = DynmapBufferedImage.allocateBufferedImage(tileWidth, tileHeight);
        day_argb_buf[i] = dayim[i].argb_buf;
      }
    }

    /* Create perspective state object */
    OurPerspectiveState ps = new OurPerspectiveState(mapiter, isnether);

    ps.top = new Vector3D();
    ps.bottom = new Vector3D();
    double xbase = tile.tx * tileWidth;
    double ybase = tile.ty * tileHeight;
    boolean shaderdone[] = new boolean[numshaders];
    boolean rendered[] = new boolean[numshaders];
    for (int x = 0; x < tileWidth; x++) {
      ps.px = x;
      for (int y = 0; y < tileHeight; y++) {
        ps.top.x =
            ps.bottom.x =
                xbase + x + 0.5; /* Start at center of pixel at Y=127.5, bottom at Y=-0.5 */
        ps.top.y = ps.bottom.y = ybase + y + 0.5;
        ps.top.z = maxheight + 0.5;
        ps.bottom.z = minheight - 0.5;
        map_to_world.transform(ps.top); /* Transform to world coordinates */
        map_to_world.transform(ps.bottom);
        ps.py = y;
        for (int i = 0; i < numshaders; i++) {
          shaderstate[i].reset(ps);
        }
        ps.raytrace(cache, mapiter, shaderstate, shaderdone);
        for (int i = 0; i < numshaders; i++) {
          if (shaderdone[i] == false) {
            shaderstate[i].rayFinished(ps);
          } else {
            shaderdone[i] = false;
            rendered[i] = true;
          }
          shaderstate[i].getRayColor(rslt, 0);
          argb_buf[i][(tileHeight - y - 1) * tileWidth + x] = rslt.getARGB();
          if (day_argb_buf[i] != null) {
            shaderstate[i].getRayColor(rslt, 1);
            day_argb_buf[i][(tileHeight - y - 1) * tileWidth + x] = rslt.getARGB();
          }
        }
      }
    }

    boolean renderone = false;
    /* Test to see if we're unchanged from older tile */
    TileHashManager hashman = MapManager.mapman.hashman;
    for (int i = 0; i < numshaders; i++) {
      long crc = hashman.calculateTileHash(argb_buf[i]);
      boolean tile_update = false;
      String prefix = shaderstate[i].getMap().getPrefix();
      if (rendered[i]) {
        renderone = true;
        MapType.ImageFormat fmt = shaderstate[i].getMap().getImageFormat();
        String fname = tile.getFilename(prefix, fmt);
        File f = new File(tile.getDynmapWorld().worldtilepath, fname);
        FileLockManager.getWriteLock(f);
        try {
          if ((!f.exists())
              || (crc != hashman.getImageHashCode(tile.getKey(), prefix, tile.tx, tile.ty))) {
            /* Wrap buffer as buffered image */
            Debug.debug("saving image " + f.getPath());
            if (!f.getParentFile().exists()) f.getParentFile().mkdirs();
            try {
              FileLockManager.imageIOWrite(im[i].buf_img, fmt.getFileExt(), f);
            } catch (IOException e) {
              Debug.error("Failed to save image: " + f.getPath(), e);
            } catch (java.lang.NullPointerException e) {
              Debug.error("Failed to save image (NullPointerException): " + f.getPath(), e);
            }
            MapManager.mapman.pushUpdate(tile.getWorld(), new Client.Tile(fname));
            hashman.updateHashCode(tile.getKey(), prefix, tile.tx, tile.ty, crc);
            tile.getDynmapWorld().enqueueZoomOutUpdate(f);
            tile_update = true;
          } else {
            Debug.debug("skipping image " + f.getPath() + " - hash match");
          }
        } finally {
          FileLockManager.releaseWriteLock(f);
          DynmapBufferedImage.freeBufferedImage(im[i]);
        }
        MapManager.mapman.updateStatistics(tile, prefix, true, tile_update, !rendered[i]);
        /* Handle day image, if needed */
        if (dayim[i] != null) {
          fname = tile.getDayFilename(prefix, fmt);
          f = new File(tile.getDynmapWorld().worldtilepath, fname);
          FileLockManager.getWriteLock(f);
          prefix = prefix + "_day";
          tile_update = false;
          try {
            if ((!f.exists())
                || (crc != hashman.getImageHashCode(tile.getKey(), prefix, tile.tx, tile.ty))) {
              /* Wrap buffer as buffered image */
              Debug.debug("saving image " + f.getPath());
              if (!f.getParentFile().exists()) f.getParentFile().mkdirs();
              try {
                FileLockManager.imageIOWrite(dayim[i].buf_img, fmt.getFileExt(), f);
              } catch (IOException e) {
                Debug.error("Failed to save image: " + f.getPath(), e);
              } catch (java.lang.NullPointerException e) {
                Debug.error("Failed to save image (NullPointerException): " + f.getPath(), e);
              }
              MapManager.mapman.pushUpdate(tile.getWorld(), new Client.Tile(fname));
              hashman.updateHashCode(tile.getKey(), prefix, tile.tx, tile.ty, crc);
              tile.getDynmapWorld().enqueueZoomOutUpdate(f);
              tile_update = true;
            } else {
              Debug.debug("skipping image " + f.getPath() + " - hash match");
            }
          } finally {
            FileLockManager.releaseWriteLock(f);
            DynmapBufferedImage.freeBufferedImage(dayim[i]);
          }
          MapManager.mapman.updateStatistics(tile, prefix, true, tile_update, !rendered[i]);
        }
      }
    }
    return renderone;
  }
  @Override
  public List<DynmapChunk> getRequiredChunks(MapTile tile) {
    if (!(tile instanceof HDMapTile)) return Collections.emptyList();

    HDMapTile t = (HDMapTile) tile;
    int min_chunk_x = Integer.MAX_VALUE;
    int max_chunk_x = Integer.MIN_VALUE;
    int min_chunk_z = Integer.MAX_VALUE;
    int max_chunk_z = Integer.MIN_VALUE;

    /* Make corners for volume: 0 = bottom-lower-left, 1 = top-lower-left, 2=bottom-upper-left, 3=top-upper-left
     * 4 = bottom-lower-right, 5 = top-lower-right, 6 = bottom-upper-right, 7 = top-upper-right */
    Vector3D corners[] = new Vector3D[8];
    int[] chunk_x = new int[8];
    int[] chunk_z = new int[8];
    for (int x = t.tx, idx = 0; x <= (t.tx + 1); x++) {
      for (int y = t.ty; y <= (t.ty + 1); y++) {
        for (int z = 0; z <= 1; z++) {
          corners[idx] = new Vector3D();
          corners[idx].x = x * tileWidth;
          corners[idx].y = y * tileHeight;
          corners[idx].z = z * 128;
          map_to_world.transform(corners[idx]);
          /* Compute chunk coordinates of corner */
          chunk_x[idx] = (int) Math.floor(corners[idx].x / 16);
          chunk_z[idx] = (int) Math.floor(corners[idx].z / 16);
          /* Compute min/max of chunk coordinates */
          if (min_chunk_x > chunk_x[idx]) min_chunk_x = chunk_x[idx];
          if (max_chunk_x < chunk_x[idx]) max_chunk_x = chunk_x[idx];
          if (min_chunk_z > chunk_z[idx]) min_chunk_z = chunk_z[idx];
          if (max_chunk_z < chunk_z[idx]) max_chunk_z = chunk_z[idx];
          idx++;
        }
      }
    }
    /* Make rectangles of X-Z projection of each side of the tile volume, 0 = top, 1 = bottom, 2 = left, 3 = right,
     * 4 = upper, 5 = lower */
    Rectangle rect[] = new Rectangle[6];
    rect[0] = new Rectangle(corners[1], corners[3], corners[5]);
    rect[1] = new Rectangle(corners[0], corners[2], corners[4]);
    rect[2] = new Rectangle(corners[0], corners[1], corners[2]);
    rect[3] = new Rectangle(corners[4], corners[5], corners[6]);
    rect[4] = new Rectangle(corners[2], corners[3], corners[6]);
    rect[5] = new Rectangle(corners[0], corners[1], corners[4]);

    /* Now, need to walk through the min/max range to see which chunks are actually needed */
    ArrayList<DynmapChunk> chunks = new ArrayList<DynmapChunk>();
    Rectangle chunkrect = new Rectangle();
    int misscnt = 0;
    for (int x = min_chunk_x; x <= max_chunk_x; x++) {
      for (int z = min_chunk_z; z <= max_chunk_z; z++) {
        chunkrect.setSquare(x * 16, z * 16, 16);
        boolean hit = false;
        /* Check to see if square of chunk intersects any of our rectangle sides */
        for (int rctidx = 0; (!hit) && (rctidx < rect.length); rctidx++) {
          if (chunkrect.testRectangleIntesectsRectangle(rect[rctidx])) {
            hit = true;
          }
        }
        if (hit) {
          DynmapChunk chunk = new DynmapChunk(x, z);
          chunks.add(chunk);
        } else {
          misscnt++;
        }
      }
    }
    return chunks;
  }