public List<OrderedCanvasIdPair> getSortedNeighborPairs()
      throws IOException, InterruptedException {

    LOG.info("getSortedNeighborPairs: entry");

    final List<Double> zValues =
        renderDataClient.getStackZValues(parameters.stack, parameters.minZ, parameters.maxZ);

    final Map<Double, TileBoundsRTree> zToTreeMap = buildRTrees(zValues);

    final Set<OrderedCanvasIdPair> existingPairs = getExistingPairs();
    final Set<OrderedCanvasIdPair> neighborPairs = new TreeSet<>();

    Double z;
    Double neighborZ;
    TileBoundsRTree currentZTree;
    List<TileBoundsRTree> neighborTreeList;
    Set<OrderedCanvasIdPair> currentNeighborPairs;
    for (int zIndex = 0; zIndex < zValues.size(); zIndex++) {

      z = zValues.get(zIndex);
      currentZTree = zToTreeMap.get(z);

      neighborTreeList = new ArrayList<>();

      final double maxNeighborZ = Math.min(parameters.maxZ, z + parameters.zNeighborDistance);

      for (int neighborZIndex = zIndex + 1; neighborZIndex < zValues.size(); neighborZIndex++) {
        neighborZ = zValues.get(neighborZIndex);
        if (neighborZ > maxNeighborZ) {
          break;
        }
        neighborTreeList.add(zToTreeMap.get(neighborZ));
      }

      currentNeighborPairs =
          currentZTree.getCircleNeighbors(
              neighborTreeList,
              parameters.xyNeighborFactor,
              parameters.excludeCornerNeighbors,
              parameters.excludeSameLayerNeighbors,
              parameters.excludeSameSectionNeighbors);
      if (existingPairs.size() > 0) {
        final int beforeSize = currentNeighborPairs.size();
        currentNeighborPairs.removeAll(existingPairs);
        final int afterSize = currentNeighborPairs.size();
        LOG.info("removed {} existing pairs for z {}", (beforeSize - afterSize), z);
      }

      neighborPairs.addAll(currentNeighborPairs);
    }

    LOG.info("getSortedNeighborPairs: exit, returning {} pairs", neighborPairs.size());

    return new ArrayList<>(neighborPairs);
  }
  @Nonnull
  private Map<Double, TileBoundsRTree> buildRTrees(final List<Double> zValues) throws IOException {

    final Map<Double, TileBoundsRTree> zToTreeMap = new LinkedHashMap<>();

    long totalTileCount = 0;
    long filteredTileCount = 0;

    for (final Double z : zValues) {

      List<TileBounds> tileBoundsList = renderDataClient.getTileBounds(parameters.stack, z);
      TileBoundsRTree tree = new TileBoundsRTree(z, tileBoundsList);

      totalTileCount += tileBoundsList.size();

      if (filterTilesWithBox) {

        final int unfilteredCount = tileBoundsList.size();

        tileBoundsList =
            tree.findTilesInBox(
                parameters.minX, parameters.minY,
                parameters.maxX, parameters.maxY);

        if (unfilteredCount > tileBoundsList.size()) {

          LOG.info(
              "buildRTrees: removed {} tiles outside of bounding box",
              (unfilteredCount - tileBoundsList.size()));

          tree = new TileBoundsRTree(z, tileBoundsList);
        }
      }

      if (parameters.excludeCompletelyObscuredTiles) {

        final int unfilteredCount = tileBoundsList.size();

        tileBoundsList = tree.findVisibleTiles();

        if (unfilteredCount > tileBoundsList.size()) {

          LOG.info(
              "buildRTrees: removed {} completely obscured tiles",
              (unfilteredCount - tileBoundsList.size()));

          tree = new TileBoundsRTree(z, tileBoundsList);
        }
      }

      zToTreeMap.put(z, tree);

      filteredTileCount += tileBoundsList.size();
    }

    LOG.info(
        "buildRTrees: added bounds for {} out of {} tiles to {} trees",
        filteredTileCount,
        totalTileCount,
        zToTreeMap.size());

    return zToTreeMap;
  }