protected static void create(
      List<MapReader> target, RenderingContext context, PJsonObject params) {
    String map = params.getString("map");
    String group = params.getString("group");
    int metaTileWidth = params.getInt("metaTileWidth");
    int metaTileHeight = params.getInt("metaTileHeight");
    String units = context.getGlobalParams().getString("units");

    target.add(
        new KaMapCacheMapReader(map, group, units, metaTileWidth, metaTileHeight, context, params));
  }
  private KaMapCacheMapReader(
      String map,
      String group,
      String units,
      int metaTileWidth,
      int metaTileHeight,
      RenderingContext context,
      PJsonObject params) {
    super(context, params);
    this.map = map;
    this.group = group;
    this.units = units;
    this.metaTileWidth = metaTileWidth;
    this.metaTileHeight = metaTileHeight;

    PJsonArray maxExtent = params.getJSONArray("maxExtent");
    PJsonArray tileSize = params.getJSONArray("tileSize");
    tileCacheLayerInfo =
        new TileCacheLayerInfo(
            params.getJSONArray("resolutions"),
            tileSize.getInt(0),
            tileSize.getInt(1),
            maxExtent.getFloat(0),
            maxExtent.getFloat(1),
            maxExtent.getFloat(2),
            maxExtent.getFloat(3),
            params.getString("extension"));
  }
  private WMTSMapReader(String layer, RenderingContext context, PJsonObject params) {
    super(context, params);
    this.layer = layer;
    this.capabilitiesInfo = WMTSServiceInfo.getLayerInfo(baseUrl, layer, context);
    // Optional (but mandatory if matrixIds is not provided)
    PJsonArray maxExtent = params.optJSONArray(TILE_FULL_EXTENT, params.optJSONArray(MAX_EXTENT));
    // Optional (but mandatory if matrixIds is not provided)
    PJsonArray tileSize = params.optJSONArray(TILE_SIZE);
    opacity = params.optFloat(OPACITY, 1.0F);
    version = params.optString(VERSION, "1.0.0");
    requestEncoding =
        WMTSRequestEncoding.valueOf(
            params.optString(REQUEST_ENCODING, WMTSRequestEncoding.REST.name()));

    // Optional (but mandatory if matrixIds is not provided)
    tileOrigin = params.optJSONArray(TILE_ORIGIN);
    style = params.optString(STYLE, "");
    // Optional
    dimensions = params.optJSONArray(DIMENSIONS);
    // Optional
    dimensionsParams = params.optJSONObject(PARAMS);
    matrixSet = params.getString(MATRIX_SET);
    // Optional (but mandatory if matrixIds is not provided)
    zoomOffset = params.optInt(ZOOM_OFFSET);
    // Optional
    matrixIds = params.optJSONArray(MATRIX_IDS);
    // Optional (but mandatory if matrixIds is not provided)
    formatSuffix = params.optString(FORMAT_SUFFIX, params.optString(EXTENSION));

    // Optional (but mandatory if matrixIds is provided and requestEncoding is KVP)
    format = params.optString(FORMAT);

    if (tileOrigin == null && matrixIds == null) {
      throw new IllegalArgumentException(
          "Either " + TILE_ORIGIN + " or " + MATRIX_IDS + " is required.");
    }
    if (zoomOffset == null && matrixIds == null) {
      throw new IllegalArgumentException(
          "Either " + ZOOM_OFFSET + " or " + MATRIX_IDS + " is required.");
    }
    if (formatSuffix == null && matrixIds == null) {
      throw new IllegalArgumentException(
          "Either " + EXTENSION + " (or " + FORMAT_SUFFIX + ") or " + MATRIX_IDS + " is required.");
    }
    if (matrixIds == null) {
      tileCacheLayerInfo =
          new WMTSLayerInfo(
              params.getJSONArray(RESOLUTIONS),
              tileSize.getInt(0),
              tileSize.getInt(1),
              maxExtent.getFloat(0),
              maxExtent.getFloat(1),
              maxExtent.getFloat(2),
              maxExtent.getFloat(3),
              formatSuffix);
    }
  }
 @Override
 public List<? extends MapReader> create(
     String type, RenderingContext context, PJsonObject params) {
   return Collections.singletonList(
       new WMTSMapReader(params.getString("layer"), context, params));
 }
  @Override
  protected URI getTileUri(
      URI commonUri,
      Transformer transformer,
      double minGeoX,
      double minGeoY,
      double maxGeoX,
      double maxGeoY,
      long w,
      long h)
      throws URISyntaxException, UnsupportedEncodingException {
    if (matrixIds != null) {
      PJsonArray topLeftCorner = matrix.getJSONArray(TOP_LEFT_CORNER);
      float factor = 1 / (matrix.getFloat(RESOLUTION) * w);
      int row = (int) Math.round((topLeftCorner.getDouble(1) - maxGeoY) * factor);
      int col = (int) Math.round((minGeoX - topLeftCorner.getDouble(0)) * factor);
      if (WMTSRequestEncoding.REST == requestEncoding) {
        String path = commonUri.getPath();
        for (int i = 0; i < dimensions.size(); i++) {
          String d = dimensions.getString(i);
          path = path.replace("{" + d + "}", dimensionsParams.getString(d.toUpperCase()));
        }
        path = path.replace("{TileMatrixSet}", matrixSet);
        path = path.replace("{TileMatrix}", matrix.getString("identifier"));
        path = path.replace("{TileRow}", String.valueOf(row));
        path = path.replace("{TileCol}", String.valueOf(col));

        return new URI(
            commonUri.getScheme(),
            commonUri.getUserInfo(),
            commonUri.getHost(),
            commonUri.getPort(),
            path,
            commonUri.getQuery(),
            commonUri.getFragment());
      } else {
        String query = "SERVICE=WMTS";
        query += "&REQUEST=GetTile";
        query += "&VERSION=" + version;
        query += "&LAYER=" + layer;
        query += "&STYLE=" + style;
        query += "&TILEMATRIXSET=" + matrixSet;
        query += "&TILEMATRIX=" + matrix.getString("identifier");
        query += "&TILEROW=" + row;
        query += "&TILECOL=" + col;
        query += "&FORMAT=" + format;
        if (dimensions != null) {
          for (int i = 0; i < dimensions.size(); i++) {
            String d = dimensions.getString(i);
            query += "&" + d + "=" + dimensionsParams.getString(d.toUpperCase());
          }
        }
        return new URI(
            commonUri.getScheme(),
            commonUri.getUserInfo(),
            commonUri.getHost(),
            commonUri.getPort(),
            commonUri.getPath(),
            query,
            commonUri.getFragment());
      }
    } else {
      double targetResolution = (maxGeoX - minGeoX) / w;
      WMTSLayerInfo.ResolutionInfo resolution =
          tileCacheLayerInfo.getNearestResolution(targetResolution);

      int col =
          (int)
              Math.round(
                  Math.floor(
                      ((maxGeoX + minGeoX) / 2 - tileOrigin.getDouble(0))
                          / (resolution.value * w)));
      int row =
          (int)
              Math.round(
                  Math.floor(
                      (tileOrigin.getDouble(1) - (maxGeoY + minGeoY) / 2)
                          / (resolution.value * h)));

      StringBuilder path = new StringBuilder();
      if (!commonUri.getPath().endsWith("/")) {
        path.append('/');
      }
      if (requestEncoding == WMTSRequestEncoding.REST) {
        path.append(version);
        path.append('/').append(layer);
        path.append('/').append(style);
        // Add dimensions
        if (dimensions != null) {
          for (int i = 0; i < dimensions.size(); i++) {
            path.append('/').append(dimensionsParams.getString(dimensions.getString(i)));
          }
        }
        path.append('/').append(matrixSet);
        path.append('/').append(resolution.index + zoomOffset);
        path.append('/').append(row);
        path.append('/').append(col);

        path.append('.').append(tileCacheLayerInfo.getExtension());

        return new URI(
            commonUri.getScheme(),
            commonUri.getUserInfo(),
            commonUri.getHost(),
            commonUri.getPort(),
            commonUri.getPath() + path,
            commonUri.getQuery(),
            commonUri.getFragment());
      } else {
        String query = "SERVICE=WMTS";
        query += "&REQUEST=GetTile";
        query += "&VERSION=" + version;
        query += "&LAYER=" + layer;
        query += "&STYLE=" + style;
        query += "&TILEMATRIXSET=" + matrixSet;
        String tileMatrix = "" + (resolution.index + zoomOffset);
        if (capabilitiesInfo != null && capabilitiesInfo.getTileMatrices().containsKey(matrixSet)) {
          final WMTSServiceInfo.TileMatrixSet tileMatrixSet =
              capabilitiesInfo.getTileMatrices().get(matrixSet);
          if (!tileMatrixSet.limits.containsKey(tileMatrix)) {
            // try to find a tileMatrix from capabilities that seems to match parameters
            final WMTSServiceInfo.TileMatrixLimit limit =
                tileMatrixSet.limits.get(matrixSet + ":" + tileMatrix);
            if (limit != null) {
              tileMatrix = limit.id;
            } else {
              for (WMTSServiceInfo.TileMatrixLimit l : tileMatrixSet.limits.values()) {
                if (l.id.endsWith(":" + tileMatrix)) {
                  tileMatrix = l.id;
                  break;
                }
              }
            }
          }
        }
        query += "&TILEMATRIX=" + tileMatrix;
        query += "&TILEROW=" + row;
        query += "&TILECOL=" + col;
        query += "&FORMAT=" + (formatSuffix.equals("png") ? "image/png" : "image/jpeg");
        if (dimensions != null) {
          for (int i = 0; i < dimensions.size(); i++) {
            String d = dimensions.getString(i);
            query += "&" + d + "=" + dimensionsParams.getString(d.toUpperCase());
          }
        }
        return new URI(
            commonUri.getScheme(),
            commonUri.getUserInfo(),
            commonUri.getHost(),
            commonUri.getPort(),
            commonUri.getPath(),
            query,
            commonUri.getFragment());
      }
    }
  }
  /**
   * Do the actual work of creating the PDF temporary file.
   *
   * @throws InterruptedException
   */
  protected TempFile doCreatePDFFile(String spec, HttpServletRequest httpServletRequest)
      throws IOException, DocumentException, ServletException, InterruptedException {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Generating PDF for spec=" + spec);
    }

    if (SPEC_LOGGER.isInfoEnabled()) {
      SPEC_LOGGER.info(spec);
    }

    PJsonObject specJson = MapPrinter.parseSpec(spec);
    if (specJson.has("app")) {
      app = specJson.getString("app");
    } else {
      app = null;
    }

    MapPrinter mapPrinter = getMapPrinter(app);

    Map<String, String> headers = new HashMap<String, String>();
    TreeSet<String> configHeaders = mapPrinter.getConfig().getHeaders();
    if (configHeaders == null) {
      configHeaders = new TreeSet<String>();
      configHeaders.add("Referer");
      configHeaders.add("Cookie");
    }
    for (Iterator<String> header_iter = configHeaders.iterator(); header_iter.hasNext(); ) {
      String header = header_iter.next();
      if (httpServletRequest.getHeader(header) != null) {
        headers.put(header, httpServletRequest.getHeader(header));
      }
    }

    final OutputFormat outputFormat = mapPrinter.getOutputFormat(specJson);
    // create a temporary file that will contain the PDF
    final File tempJavaFile =
        File.createTempFile(
            TEMP_FILE_PREFIX, "." + outputFormat.getFileSuffix() + TEMP_FILE_SUFFIX, getTempDir());
    TempFile tempFile = new TempFile(tempJavaFile, specJson, outputFormat);

    FileOutputStream out = null;
    try {
      out = new FileOutputStream(tempFile);
      mapPrinter.print(specJson, out, headers);

      return tempFile;
    } catch (IOException e) {
      deleteFile(tempFile);
      throw e;
    } catch (DocumentException e) {
      deleteFile(tempFile);
      throw e;
    } catch (InterruptedException e) {
      deleteFile(tempFile);
      throw e;
    } finally {
      if (out != null) {
        out.close();
      }
    }
  }