private void readTileData(
        File outputFile,
        int tileX,
        int tileY,
        int tileWidth,
        int tileHeight,
        int jp2TileX,
        int jp2TileY,
        int jp2TileWidth,
        int jp2TileHeight,
        short[] tileData,
        Rectangle destRect)
        throws IOException {

      synchronized (this) {
        if (!locks.containsKey(outputFile)) {
          locks.put(outputFile, new Object());
        }
      }
      final Object lock = locks.get(outputFile);

      synchronized (lock) {
        Jp2File jp2File = getOpenJ2pFile(outputFile);

        int jp2Width = jp2File.width;
        int jp2Height = jp2File.height;
        if (jp2Width > jp2TileWidth || jp2Height > jp2TileHeight) {
          throw new IllegalStateException(
              String.format(
                  "width (=%d) > tileWidth (=%d) || height (=%d) > tileHeight (=%d)",
                  jp2Width, jp2TileWidth, jp2Height, jp2TileHeight));
        }

        int jp2X = destRect.x - jp2TileX * jp2TileWidth;
        int jp2Y = destRect.y - jp2TileY * jp2TileHeight;
        if (jp2X < 0 || jp2Y < 0) {
          throw new IllegalStateException(
              String.format("jp2X (=%d) < 0 || jp2Y (=%d) < 0", jp2X, jp2Y));
        }

        final ImageInputStream stream = jp2File.stream;

        if (jp2X == 0
            && jp2Width == tileWidth
            && jp2Y == 0
            && jp2Height == tileHeight
            && tileWidth * tileHeight == tileData.length) {
          stream.seek(jp2File.dataPos);
          stream.readFully(tileData, 0, tileData.length);
        } else {
          final Rectangle jp2FileRect = new Rectangle(0, 0, jp2Width, jp2Height);
          final Rectangle tileRect = new Rectangle(jp2X, jp2Y, tileWidth, tileHeight);
          final Rectangle intersection = jp2FileRect.intersection(tileRect);
          System.out.printf(
              "%s: tile=(%d,%d): jp2FileRect=%s, tileRect=%s, intersection=%s\n",
              jp2File.file, tileX, tileY, jp2FileRect, tileRect, intersection);
          if (!intersection.isEmpty()) {
            long seekPos =
                jp2File.dataPos + NUM_SHORT_BYTES * (intersection.y * jp2Width + intersection.x);
            int tilePos = 0;
            for (int y = 0; y < intersection.height; y++) {
              stream.seek(seekPos);
              stream.readFully(tileData, tilePos, intersection.width);
              seekPos += NUM_SHORT_BYTES * jp2Width;
              tilePos += tileWidth;
              for (int x = intersection.width; x < tileWidth; x++) {
                tileData[y * tileWidth + x] = (short) 0;
              }
            }
            for (int y = intersection.height; y < tileWidth; y++) {
              for (int x = 0; x < tileWidth; x++) {
                tileData[y * tileWidth + x] = (short) 0;
              }
            }
          } else {
            Arrays.fill(tileData, (short) 0);
          }
        }
      }
    }
    @Override
    protected void computeRect(PlanarImage[] sources, WritableRaster dest, Rectangle destRect) {
      final DataBufferUShort dataBuffer = (DataBufferUShort) dest.getDataBuffer();
      final short[] tileData = dataBuffer.getData();

      final int tileWidth = this.getTileWidth();
      final int tileHeight = this.getTileHeight();
      final int tileX = destRect.x / tileWidth;
      final int tileY = destRect.y / tileHeight;

      if (tileWidth * tileHeight != tileData.length) {
        throw new IllegalStateException(
            String.format(
                "tileWidth (=%d) * tileHeight (=%d) != tileData.length (=%d)",
                tileWidth, tileHeight, tileData.length));
      }

      final int resolution = getLevel();
      final Dimension jp2TileDim = getJp2TileDim(bandInfo, resolution);

      final int jp2TileWidth = jp2TileDim.width;
      final int jp2TileHeight = jp2TileDim.height;
      final int jp2TileX = destRect.x / jp2TileWidth;
      final int jp2TileY = destRect.y / jp2TileHeight;

      // Res - Img Size - Tile W
      //  0  -  10960   -  4096
      //  1  -   5480   -  2048
      //  2  -   2740   -  1024
      //  3  -   1370   -   512
      //  4  -    685   -   256
      //  5  -    343   -   128

      final File outputFile =
          new File(
              cacheDir,
              FileUtils.exchangeExtension(
                  imageFile.getName(),
                  String.format("_R%d_TX%d_TY%d.pgx", resolution, jp2TileX, jp2TileY)));
      final File outputFile0 = getFirstComponentOutputFile(outputFile);
      if (!outputFile0.exists()) {
        System.out.printf(
            "Jp2ExeImage.readTileData(): recomputing res=%d, tile=(%d,%d)\n",
            resolution, jp2TileX, jp2TileY);
        try {
          decompressTile(outputFile, jp2TileX, jp2TileY);
        } catch (IOException e) {
          // warn
          outputFile0.delete();
        }
        if (!outputFile0.exists()) {
          Arrays.fill(tileData, (short) 0);
          return;
        }
      }

      try {
        System.out.printf(
            "Jp2ExeImage.readTileData(): reading res=%d, tile=(%d,%d)\n",
            resolution, jp2TileX, jp2TileY);
        readTileData(
            outputFile0,
            tileX,
            tileY,
            tileWidth,
            tileHeight,
            jp2TileX,
            jp2TileY,
            jp2TileWidth,
            jp2TileHeight,
            tileData,
            destRect);
      } catch (IOException e) {
        // warn
      }
    }