/**
   * Apply the model's position, orientation, and scale to a COLLADA root.
   *
   * @param root COLLADA root to configure.
   */
  protected void configureColladaRoot(ColladaRoot root) {
    root.setResourceResolver(this);

    Position refPosition = this.model.getLocation().getPosition();
    root.setPosition(refPosition);
    root.setAltitudeMode(KMLUtil.convertAltitudeMode(this.model.getAltitudeMode()));

    KMLOrientation orientation = this.model.getOrientation();
    if (orientation != null) {
      Double d = orientation.getHeading();
      if (d != null) root.setHeading(Angle.fromDegrees(d));

      d = orientation.getTilt();
      if (d != null) root.setPitch(Angle.fromDegrees(-d));

      d = orientation.getRoll();
      if (d != null) root.setRoll(Angle.fromDegrees(-d));
    }

    KMLScale scale = this.model.getScale();
    if (scale != null) {
      Double x = scale.getX();
      Double y = scale.getY();
      Double z = scale.getZ();

      Vec4 modelScale = new Vec4(x != null ? x : 1.0, y != null ? y : 1.0, z != null ? z : 1.0);

      root.setModelScale(modelScale);
    }
  }
  protected void onHorizontalTranslateRel(
      double forwardInput,
      double sideInput,
      double totalForwardInput,
      double totalSideInput,
      ViewInputAttributes.DeviceAttributes deviceAttributes,
      ViewInputAttributes.ActionAttributes actionAttributes) {
    Angle forwardChange;
    Angle sideChange;

    this.stopGoToAnimators();
    if (actionAttributes.getMouseActions() != null) {
      forwardChange =
          Angle.fromDegrees(
              -totalForwardInput * getScaleValueElevation(deviceAttributes, actionAttributes));
      sideChange =
          Angle.fromDegrees(
              totalSideInput * getScaleValueElevation(deviceAttributes, actionAttributes));
    } else {
      forwardChange =
          Angle.fromDegrees(
              forwardInput * speed * getScaleValueElevation(deviceAttributes, actionAttributes));
      sideChange =
          Angle.fromDegrees(
              sideInput * speed * getScaleValueElevation(deviceAttributes, actionAttributes));
    }
    onHorizontalTranslateRel(forwardChange, sideChange, actionAttributes);
  }
 protected static Angle clampAngle(Angle a, Angle min, Angle max) {
   double degrees = a.degrees;
   double minDegrees = min.degrees;
   double maxDegrees = max.degrees;
   return Angle.fromDegrees(
       degrees < minDegrees ? minDegrees : (degrees > maxDegrees ? maxDegrees : degrees));
 }
  protected void onHorizontalTranslateRel(
      Angle forwardChange, Angle sideChange, ViewInputAttributes.ActionAttributes actionAttribs) {
    View view = this.getView();
    if (view == null) // include this test to ensure any derived implementation performs it
    {
      return;
    }

    if (forwardChange.equals(Angle.ZERO) && sideChange.equals(Angle.ZERO)) {
      return;
    }

    if (view instanceof BasicFlyView) {

      Vec4 forward = view.getForwardVector();
      Vec4 up = view.getUpVector();
      Vec4 side = forward.transformBy3(Matrix.fromAxisAngle(Angle.fromDegrees(90), up));

      forward = forward.multiply3(forwardChange.getDegrees());
      side = side.multiply3(sideChange.getDegrees());
      Vec4 eyePoint = view.getEyePoint();
      eyePoint = eyePoint.add3(forward.add3(side));
      Position newPosition = view.getGlobe().computePositionFromPoint(eyePoint);

      this.setEyePosition(this.uiAnimControl, view, newPosition, actionAttribs);
      view.firePropertyChange(AVKey.VIEW, null, view);
    }
  }
 static Angle computeColumnLongitude(int column, Angle delta) {
   if (delta == null) {
     String msg = Logging.getMessage("nullValue.AngleIsNull");
     Logging.logger().severe(msg);
     throw new IllegalArgumentException(msg);
   }
   return Angle.fromDegrees(-180 + delta.getDegrees() * column);
 }
 static Angle computeRowLatitude(int row, Angle delta) {
   if (delta == null) {
     String msg = Logging.getMessage("nullValue.AngleIsNull");
     Logging.logger().severe(msg);
     throw new IllegalArgumentException(msg);
   }
   return Angle.fromDegrees(-90d + delta.getDegrees() * row);
 }
  protected void onRotateView(
      double headingInput,
      double pitchInput,
      double totalHeadingInput,
      double totalPitchInput,
      ViewInputAttributes.DeviceAttributes deviceAttributes,
      ViewInputAttributes.ActionAttributes actionAttributes) {

    Angle headingChange;
    Angle pitchChange;
    this.stopGoToAnimators();
    headingChange =
        Angle.fromDegrees(
            totalHeadingInput * getScaleValueElevation(deviceAttributes, actionAttributes));
    pitchChange =
        Angle.fromDegrees(
            totalPitchInput * getScaleValueElevation(deviceAttributes, actionAttributes));
    onRotateView(headingChange, pitchChange, actionAttributes);
  }
Example #8
0
 /**
  * Computes the lat/lon of the pickPoint over the world map
  *
  * @param dc the current <code>DrawContext</code>
  * @param locationSW the screen location of the bottom left corner of the map
  * @param mapSize the world map screen dimension in pixels
  * @return the picked Position
  */
 protected Position computePickPosition(DrawContext dc, Vec4 locationSW, Dimension mapSize) {
   Position pickPosition = null;
   Point pickPoint = dc.getPickPoint();
   if (pickPoint != null) {
     Rectangle viewport = dc.getView().getViewport();
     // Check if pickpoint is inside the map
     if (pickPoint.getX() >= locationSW.getX()
         && pickPoint.getX() < locationSW.getX() + mapSize.width
         && viewport.height - pickPoint.getY() >= locationSW.getY()
         && viewport.height - pickPoint.getY() < locationSW.getY() + mapSize.height) {
       double lon = (pickPoint.getX() - locationSW.getX()) / mapSize.width * 360 - 180;
       double lat =
           (viewport.height - pickPoint.getY() - locationSW.getY()) / mapSize.height * 180 - 90;
       double pickAltitude = 1000e3;
       pickPosition = new Position(Angle.fromDegrees(lat), Angle.fromDegrees(lon), pickAltitude);
     }
   }
   return pickPosition;
 }
  /**
   * Called when the roll changes due to user input.
   *
   * @param rollInput Change in roll.
   * @param deviceAttributes Attributes of the input device.
   * @param actionAttributes Action that caused the change.
   */
  protected void onRoll(
      double rollInput,
      ViewInputAttributes.DeviceAttributes deviceAttributes,
      ViewInputAttributes.ActionAttributes actionAttributes) {
    Angle rollChange;
    this.stopGoToAnimators();

    rollChange =
        Angle.fromDegrees(rollInput * getScaleValueElevation(deviceAttributes, actionAttributes));

    this.onRoll(rollChange, actionAttributes);
  }
    public void actionPerformed(ActionEvent e) {
      if (!this.isEnabled()) {
        return;
      }

      if (NEW_AIRSPACE.equals(e.getActionCommand())) {
        this.createNewEntry(this.getView().getSelectedFactory());
      } else if (CLEAR_SELECTION.equals(e.getActionCommand())) {
        this.selectEntry(null, true);
      } else if (SIZE_NEW_SHAPES_TO_VIEWPORT.equals(e.getActionCommand())) {
        if (e.getSource() instanceof AbstractButton) {
          boolean selected = ((AbstractButton) e.getSource()).isSelected();
          this.setResizeNewShapesToViewport(selected);
        }
      } else if (ENABLE_EDIT.equals(e.getActionCommand())) {
        if (e.getSource() instanceof AbstractButton) {
          boolean selected = ((AbstractButton) e.getSource()).isSelected();
          this.setEnableEdit(selected);
        }
      } else if (OPEN.equals(e.getActionCommand())) {
        this.openFromFile();
      } else if (OPEN_URL.equals(e.getActionCommand())) {
        this.openFromURL();
      } else if (OPEN_DEMO_AIRSPACES.equals(e.getActionCommand())) {
        this.openFromPath(DEMO_AIRSPACES_PATH);
        this.zoomTo(
            LatLon.fromDegrees(47.6584074779224, -122.3059199579634),
            Angle.fromDegrees(-152),
            Angle.fromDegrees(75),
            750);
      } else if (REMOVE_SELECTED.equals(e.getActionCommand())) {
        this.removeEntries(Arrays.asList(this.getSelectedEntries()));
      } else if (SAVE.equals(e.getActionCommand())) {
        this.saveToFile();
      } else if (SELECTION_CHANGED.equals(e.getActionCommand())) {
        this.viewSelectionChanged();
      }
    }
/**
 * Shows how to compute terrain intersections using the highest resolution terrain data available
 * from a globe's elevation model.
 *
 * <p>To generate and show intersections, Shift + LeftClick anywhere on the globe. The program forms
 * a grid of locations around the selected location. The grid points are shown in yellow. It then
 * determines whether a line between the selected location and each grid point intersects the
 * terrain. If it does, the intersection nearest the selected location is shown in cyan and a line
 * is drawn from the selected location to the intersection. If there is no intersection, a line is
 * drawn from the selected location to the grid position.
 *
 * <p>If the highest resolution terrain is not available for the area around the selected location,
 * it is retrieved from the elevation model's source, which is most likely a remote server. Since
 * the high-res data must be retrieved and then loaded from the local disk cache, it will take some
 * time to compute and show the intersections.
 *
 * <p>This example uses a {@link gov.nasa.worldwind.terrain.Terrain} object to perform the terrain
 * retrieval, generation and intersection calculations.s
 *
 * @author tag
 * @version $Id: TerrainIntersections.java 2109 2014-06-30 16:52:38Z tgaskins $
 */
public class TerrainIntersections extends ApplicationTemplate {
  /** The width and height in degrees of the grid used to calculate intersections. */
  protected static final Angle GRID_RADIUS = Angle.fromDegrees(0.05);

  /** The number of cells along each edge of the grid. */
  protected static final int GRID_DIMENSION = 10; // cells per side

  /** The desired terrain resolution to use in the intersection calculations. */
  protected static final Double TARGET_RESOLUTION =
      10d; // meters, or null for globe's highest resolution

  protected static final int NUM_THREADS = 4; // set to 1 to run synchronously

  public static class AppFrame extends ApplicationTemplate.AppFrame {
    private static final Cursor WaitCursor = new Cursor(Cursor.WAIT_CURSOR);

    protected HighResolutionTerrain terrain;
    protected RenderableLayer gridLayer;
    protected RenderableLayer intersectionsLayer;
    protected RenderableLayer sightLinesLayer;
    protected RenderableLayer tilesLayer;
    protected Thread calculationDispatchThread;
    protected JProgressBar progressBar;
    protected ThreadPoolExecutor threadPool;
    protected List<Position> grid;
    protected int numGridPoints; // used to monitor percentage progress
    protected long startTime, endTime; // for reporting calculation duration
    protected Position previousCurrentPosition;

    public AppFrame() {
      super(true, true, false);

      // Create a thread pool.
      this.threadPool =
          new ThreadPoolExecutor(
              NUM_THREADS,
              NUM_THREADS,
              200,
              TimeUnit.MILLISECONDS,
              new LinkedBlockingQueue<Runnable>());

      // Display a progress bar.
      this.progressBar = new JProgressBar(0, 100);
      this.progressBar.setBorder(new EmptyBorder(0, 10, 0, 10));
      this.progressBar.setBorderPainted(false);
      this.progressBar.setStringPainted(true);
      this.layerPanel.add(this.progressBar, BorderLayout.SOUTH);

      // Be sure to re-use the Terrain object to take advantage of its caching.
      this.terrain = new HighResolutionTerrain(getWwd().getModel().getGlobe(), TARGET_RESOLUTION);

      this.gridLayer = new RenderableLayer();
      this.gridLayer.setName("Grid");
      this.getWwd().getModel().getLayers().add(this.gridLayer);

      this.intersectionsLayer = new RenderableLayer();
      this.intersectionsLayer.setName("Intersections");
      this.getWwd().getModel().getLayers().add(this.intersectionsLayer);

      this.sightLinesLayer = new RenderableLayer();
      this.sightLinesLayer.setName("Sight Lines");
      this.getWwd().getModel().getLayers().add(this.sightLinesLayer);

      // Set up a mouse handler to generate a grid and start intersection calculations when the user
      // shift-clicks.
      this.getWwd()
          .getInputHandler()
          .addMouseListener(
              new MouseAdapter() {
                public void mouseClicked(MouseEvent mouseEvent) {
                  // Control-Click cancels any currently running operation.
                  if ((mouseEvent.getModifiers() & ActionEvent.CTRL_MASK) != 0) {
                    if (calculationDispatchThread != null && calculationDispatchThread.isAlive())
                      calculationDispatchThread.interrupt();
                    return;
                  }

                  // Alt-Click repeats the most recent calculations.
                  if ((mouseEvent.getModifiers() & ActionEvent.ALT_MASK) != 0) {
                    if (previousCurrentPosition == null) return;

                    mouseEvent.consume(); // tell the rest of WW that this event has been processed

                    computeAndShowIntersections(previousCurrentPosition);
                    return;
                  }

                  // Perform the intersection tests in response to Shift-Click.
                  if ((mouseEvent.getModifiers() & ActionEvent.SHIFT_MASK) == 0) return;

                  mouseEvent.consume(); // tell the rest of WW that this event has been processed

                  final Position pos = getWwd().getCurrentPosition();
                  if (pos == null) return;

                  computeAndShowIntersections(pos);
                }
              });
    }

    protected void computeAndShowIntersections(final Position curPos) {
      this.previousCurrentPosition = curPos;

      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setCursor(WaitCursor);
            }
          });

      // Dispatch the calculation threads in a separate thread to avoid locking up the user
      // interface.
      this.calculationDispatchThread =
          new Thread(
              new Runnable() {
                public void run() {
                  try {
                    performIntersectionTests(curPos);
                  } catch (InterruptedException e) {
                    System.out.println("Operation was interrupted");
                  }
                }
              });

      this.calculationDispatchThread.start();
    }

    // Create containers to hold the intersection points and the lines emanating from the center.
    protected List<Position> firstIntersectionPositions = new ArrayList<Position>();
    protected List<Position[]> sightLines =
        new ArrayList<Position[]>(GRID_DIMENSION * GRID_DIMENSION);

    // Make the picked location's position and model-coordinate point available to all methods.
    protected Position referencePosition;
    protected Vec4 referencePoint;

    // This is a collection of synchronized accessors to the list updated during the calculations.

    protected synchronized void addIntersectionPosition(Position position) {
      this.firstIntersectionPositions.add(position);
    }

    protected synchronized void addSightLine(Position positionA, Position positionB) {
      this.sightLines.add(new Position[] {positionA, positionB});
    }

    protected synchronized int getSightlinesSize() {
      return this.sightLines.size();
    }

    private long lastTime = System.currentTimeMillis();

    /** Keeps the progress meter current. When calculations are complete, displays the results. */
    protected synchronized void updateProgress() {
      // Update the progress bar only once every 250 milliseconds to avoid stealing time from
      // calculations.
      if (this.sightLines.size() >= this.numGridPoints) endTime = System.currentTimeMillis();
      else if (System.currentTimeMillis() < this.lastTime + 250) return;
      this.lastTime = System.currentTimeMillis();

      // On the EDT, update the progress bar and if calculations are complete, update the World
      // Window.
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              int progress = (int) (100d * getSightlinesSize() / (double) numGridPoints);
              progressBar.setValue(progress);

              if (progress >= 100) {
                setCursor(Cursor.getDefaultCursor());
                progressBar.setString((endTime - startTime) + " ms");
                showResults();
                System.out.printf("Calculation time %d milliseconds\n", endTime - startTime);
              }
            }
          });
    }

    /** Updates the World Wind model with the new intersection locations and sight lines. */
    protected void showResults() {
      this.showIntersections(firstIntersectionPositions);
      this.showSightLines(sightLines);
      //            this.showIntersectingTiles(this.grid, this.referencePosition);
      this.getWwd().redraw();
    }

    protected void performIntersectionTests(final Position curPos) throws InterruptedException {
      // Clear the results lists when the user selects a new location.
      this.firstIntersectionPositions.clear();
      this.sightLines.clear();

      // Raise the selected location and the grid points a little above ground just to show we can.
      final double height = 5; // meters

      // Form the grid.
      double gridRadius = GRID_RADIUS.degrees;
      Sector sector =
          Sector.fromDegrees(
              curPos.getLatitude().degrees - gridRadius, curPos.getLatitude().degrees + gridRadius,
              curPos.getLongitude().degrees - gridRadius,
                  curPos.getLongitude().degrees + gridRadius);

      this.grid = buildGrid(sector, height, GRID_DIMENSION, GRID_DIMENSION);
      this.numGridPoints = grid.size();

      // Compute the position of the selected location (incorporate its height).
      this.referencePosition = new Position(curPos.getLatitude(), curPos.getLongitude(), height);
      this.referencePoint =
          terrain.getSurfacePoint(curPos.getLatitude(), curPos.getLongitude(), height);

      //            // Pre-caching is unnecessary and is useful only when it occurs before the
      // intersection
      //            // calculations. It will incur extra overhead otherwise. The normal intersection
      // calculations
      //            // cause the same caching, making subsequent calculations on the same area
      // faster.
      //            this.preCache(grid, this.referencePosition);

      // On the EDT, show the grid.
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              progressBar.setValue(0);
              progressBar.setString(null);
              clearLayers();
              showGrid(grid, referencePosition);
              getWwd().redraw();
            }
          });

      // Perform the intersection calculations.
      this.startTime = System.currentTimeMillis();
      for (Position gridPos : this.grid) // for each grid point.
      {
        //noinspection ConstantConditions
        if (NUM_THREADS > 0) this.threadPool.execute(new Intersector(gridPos));
        else performIntersection(gridPos);
      }
    }

    /**
     * Performs one line of sight calculation between the reference position and a specified grid
     * position.
     *
     * @param gridPosition the grid position.
     * @throws InterruptedException if the operation is interrupted.
     */
    protected void performIntersection(Position gridPosition) throws InterruptedException {
      // Intersect the line between this grid point and the selected position.
      Intersection[] intersections = this.terrain.intersect(this.referencePosition, gridPosition);
      if (intersections == null || intersections.length == 0) {
        // No intersection, so the line goes from the center to the grid point.
        this.sightLines.add(new Position[] {this.referencePosition, gridPosition});
        return;
      }

      // Only the first intersection is shown.
      Vec4 iPoint = intersections[0].getIntersectionPoint();
      Vec4 gPoint =
          terrain.getSurfacePoint(
              gridPosition.getLatitude(), gridPosition.getLongitude(), gridPosition.getAltitude());

      // Check to see whether the intersection is beyond the grid point.
      if (iPoint.distanceTo3(this.referencePoint) >= gPoint.distanceTo3(this.referencePoint)) {
        // Intersection is beyond the grid point; the line goes from the center to the grid point.
        this.addSightLine(this.referencePosition, gridPosition);
        return;
      }

      // Compute the position corresponding to the intersection.
      Position iPosition = this.terrain.getGlobe().computePositionFromPoint(iPoint);

      // The sight line goes from the user-selected position to the intersection position.
      this.addSightLine(this.referencePosition, new Position(iPosition, 0));

      // Keep track of the intersection positions.
      this.addIntersectionPosition(iPosition);

      this.updateProgress();
    }

    /** Inner {@link Runnable} to perform a single line/terrain intersection calculation. */
    protected class Intersector implements Runnable {
      protected final Position gridPosition;

      public Intersector(Position gridPosition) {
        this.gridPosition = gridPosition;
      }

      public void run() {
        try {
          performIntersection(this.gridPosition);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }

    protected List<Position> buildGrid(Sector sector, double height, int nLatCells, int nLonCells) {
      List<Position> grid = new ArrayList<Position>((nLatCells + 1) * (nLonCells + 1));

      double dLat = sector.getDeltaLatDegrees() / nLatCells;
      double dLon = sector.getDeltaLonDegrees() / nLonCells;

      for (int j = 0; j <= nLatCells; j++) {
        double lat =
            j == nLatCells
                ? sector.getMaxLatitude().degrees
                : sector.getMinLatitude().degrees + j * dLat;

        for (int i = 0; i <= nLonCells; i++) {
          double lon =
              i == nLonCells
                  ? sector.getMaxLongitude().degrees
                  : sector.getMinLongitude().degrees + i * dLon;

          grid.add(Position.fromDegrees(lat, lon, height));
        }
      }

      return grid;
    }

    protected void preCache(List<Position> grid, Position centerPosition)
        throws InterruptedException {
      // Pre-cache the tiles that will be needed for the intersection calculations.
      double n = 0;
      final long start = System.currentTimeMillis();
      for (Position gridPos : grid) // for each grid point.
      {
        final double progress = 100 * (n++ / grid.size());
        terrain.cacheIntersectingTiles(centerPosition, gridPos);

        SwingUtilities.invokeLater(
            new Runnable() {
              public void run() {
                progressBar.setValue((int) progress);
                progressBar.setString(null);
              }
            });
      }

      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              progressBar.setValue(100);
            }
          });

      long end = System.currentTimeMillis();
      System.out.printf(
          "Pre-caching time %d milliseconds, cache usage %f, tiles %d\n",
          end - start, terrain.getCacheUsage(), terrain.getNumCacheEntries());
    }

    protected void clearLayers() {
      this.intersectionsLayer.removeAllRenderables();
      this.sightLinesLayer.removeAllRenderables();
      this.gridLayer.removeAllRenderables();
    }

    protected void showIntersections(List<Position> intersections) {
      this.intersectionsLayer.removeAllRenderables();

      // Display the intersections as CYAN points.
      PointPlacemarkAttributes intersectionPointAttributes;
      intersectionPointAttributes = new PointPlacemarkAttributes();
      intersectionPointAttributes.setLineMaterial(Material.CYAN);
      intersectionPointAttributes.setScale(6d);
      intersectionPointAttributes.setUsePointAsDefaultImage(true);

      for (Position p : intersections) {
        PointPlacemark pm = new PointPlacemark(p);
        pm.setAltitudeMode(WorldWind.CLAMP_TO_GROUND);
        pm.setAttributes(intersectionPointAttributes);
        pm.setValue(AVKey.DISPLAY_NAME, p.toString());
        this.intersectionsLayer.addRenderable(pm);
      }
    }

    protected void showSightLines(List<Position[]> sightLines) {
      this.sightLinesLayer.removeAllRenderables();

      // Display the sight lines as green lines.
      ShapeAttributes lineAttributes;
      lineAttributes = new BasicShapeAttributes();
      lineAttributes.setDrawOutline(true);
      lineAttributes.setDrawInterior(false);
      lineAttributes.setOutlineMaterial(Material.GREEN);
      lineAttributes.setOutlineOpacity(0.6);

      for (Position[] pp : sightLines) {
        List<Position> endPoints = new ArrayList<Position>();
        endPoints.add(pp[0]);
        endPoints.add(pp[1]);

        Path path = new Path(endPoints);
        path.setAltitudeMode(WorldWind.RELATIVE_TO_GROUND);
        path.setAttributes(lineAttributes);
        this.sightLinesLayer.addRenderable(path);
      }
    }

    protected void showGridSightLines(List<Position> grid, Position cPos) {
      this.sightLinesLayer.removeAllRenderables();

      // Display lines from the center to each grid point.
      ShapeAttributes lineAttributes;
      lineAttributes = new BasicShapeAttributes();
      lineAttributes.setDrawOutline(true);
      lineAttributes.setDrawInterior(false);
      lineAttributes.setOutlineMaterial(Material.GREEN);
      lineAttributes.setOutlineOpacity(0.6);

      for (Position p : grid) {
        List<Position> endPoints = new ArrayList<Position>();
        endPoints.add(cPos);
        endPoints.add(new Position(p.getLatitude(), p.getLongitude(), 0));

        Path path = new Path(endPoints);
        path.setAltitudeMode(WorldWind.RELATIVE_TO_GROUND);
        path.setAttributes(lineAttributes);
        this.sightLinesLayer.addRenderable(path);
      }
    }

    protected void showGrid(List<Position> grid, Position cPos) {
      this.gridLayer.removeAllRenderables();

      // Display the grid points in yellow.
      PointPlacemarkAttributes gridPointAttributes;
      gridPointAttributes = new PointPlacemarkAttributes();
      gridPointAttributes.setLineMaterial(Material.YELLOW);
      gridPointAttributes.setScale(6d);
      gridPointAttributes.setUsePointAsDefaultImage(true);

      for (Position p : grid) {
        PointPlacemark pm = new PointPlacemark(p);
        pm.setAltitudeMode(WorldWind.RELATIVE_TO_GROUND);
        pm.setAttributes(gridPointAttributes);
        pm.setLineEnabled(true);
        pm.setValue(AVKey.DISPLAY_NAME, p.toString());
        this.gridLayer.addRenderable(pm);
      }

      showCenterPoint(cPos);
    }

    protected void showCenterPoint(Position cPos) {
      // Display the center point in red.
      PointPlacemarkAttributes selectedLocationAttributes;
      selectedLocationAttributes = new PointPlacemarkAttributes();
      selectedLocationAttributes.setLineMaterial(Material.RED);
      selectedLocationAttributes.setScale(8d);
      selectedLocationAttributes.setUsePointAsDefaultImage(true);

      PointPlacemark pm = new PointPlacemark(cPos);
      pm.setAltitudeMode(WorldWind.RELATIVE_TO_GROUND);
      pm.setAttributes(selectedLocationAttributes);
      pm.setValue(AVKey.DISPLAY_NAME, cPos.toString());
      pm.setLineEnabled(true);
      this.gridLayer.addRenderable(pm);
    }
  }

  public static void main(String[] args) {
    // zoom to San Francisco downtown
    Configuration.setValue(AVKey.INITIAL_ALTITUDE, 34e3);
    Configuration.setValue(AVKey.INITIAL_LATITUDE, 37.9521d);
    Configuration.setValue(AVKey.INITIAL_LONGITUDE, -119.7761d);

    // Adjust configuration values before instantiation
    ApplicationTemplate.start("World Wind Terrain Intersections", AppFrame.class);
  }
}
/**
 * @author Paul Collins
 * @version $Id: PlaceNameLayer.java 13201 2010-03-12 01:59:03Z tgaskins $
 */
public class PlaceNameLayer extends AbstractLayer implements BulkRetrievable {
  private final PlaceNameServiceSet placeNameServiceSet;
  private PriorityBlockingQueue<Runnable> requestQ = new PriorityBlockingQueue<Runnable>(64);
  private Vec4 referencePoint;
  protected final Object fileLock = new Object();
  private boolean cullNames = false;

  protected static final double LEVEL_A = 0x1 << 25;
  protected static final double LEVEL_B = 0x1 << 24;
  protected static final double LEVEL_C = 0x1 << 23;
  protected static final double LEVEL_D = 0x1 << 22;
  protected static final double LEVEL_E = 0x1 << 21;
  protected static final double LEVEL_F = 0x1 << 20;
  protected static final double LEVEL_G = 0x1 << 19;
  protected static final double LEVEL_H = 0x1 << 18;
  protected static final double LEVEL_I = 0x1 << 17;
  protected static final double LEVEL_J = 0x1 << 16;
  protected static final double LEVEL_K = 0x1 << 15;
  protected static final double LEVEL_L = 0x1 << 14;
  protected static final double LEVEL_M = 0x1 << 13;
  protected static final double LEVEL_N = 0x1 << 12;
  protected static final double LEVEL_O = 0x1 << 11;
  protected static final double LEVEL_P = 0x1 << 10;
  protected static final LatLon GRID_1x1 =
      new LatLon(Angle.fromDegrees(180d), Angle.fromDegrees(360d));
  protected static final LatLon GRID_4x8 =
      new LatLon(Angle.fromDegrees(45d), Angle.fromDegrees(45d));
  protected static final LatLon GRID_8x16 =
      new LatLon(Angle.fromDegrees(22.5d), Angle.fromDegrees(22.5d));
  protected static final LatLon GRID_16x32 =
      new LatLon(Angle.fromDegrees(11.25d), Angle.fromDegrees(11.25d));
  protected static final LatLon GRID_36x72 =
      new LatLon(Angle.fromDegrees(5d), Angle.fromDegrees(5d));
  protected static final LatLon GRID_72x144 =
      new LatLon(Angle.fromDegrees(2.5d), Angle.fromDegrees(2.5d));
  protected static final LatLon GRID_144x288 =
      new LatLon(Angle.fromDegrees(1.25d), Angle.fromDegrees(1.25d));
  protected static final LatLon GRID_288x576 =
      new LatLon(Angle.fromDegrees(0.625d), Angle.fromDegrees(0.625d));

  protected List<NavigationTile> navTiles = new ArrayList<NavigationTile>();
  // top navigation tiles for each service

  /**
   * @param placeNameServiceSet the set of PlaceNameService objects that PlaceNameLayer will render.
   * @throws IllegalArgumentException if {@link
   *     gov.nasa.worldwind.layers.placename.PlaceNameServiceSet} is null
   */
  public PlaceNameLayer(PlaceNameServiceSet placeNameServiceSet) {
    if (placeNameServiceSet == null) {
      String message = Logging.getMessage("nullValue.PlaceNameServiceSetIsNull");
      Logging.logger().fine(message);
      throw new IllegalArgumentException(message);
    }

    //
    this.placeNameServiceSet = placeNameServiceSet.deepCopy();
    for (int i = 0; i < this.placeNameServiceSet.getServiceCount(); i++) {
      // todo do this for long as well and pick min
      int calc1 =
          (int)
              (PlaceNameService.TILING_SECTOR.getDeltaLatDegrees()
                  / this.placeNameServiceSet
                      .getService(i)
                      .getTileDelta()
                      .getLatitude()
                      .getDegrees());
      int numLevels = (int) Math.log(calc1);
      navTiles.add(
          new NavigationTile(
              this.placeNameServiceSet.getService(i),
              PlaceNameService.TILING_SECTOR,
              numLevels,
              "top"));
    }

    if (!WorldWind.getMemoryCacheSet().containsCache(Tile.class.getName())) {
      long size = Configuration.getLongValue(AVKey.PLACENAME_LAYER_CACHE_SIZE, 2000000L);
      MemoryCache cache = new BasicMemoryCache((long) (0.85 * size), size);
      cache.setName("Placename Tiles");
      WorldWind.getMemoryCacheSet().addCache(Tile.class.getName(), cache);
    }
  }

  public boolean isCullNames() {
    return cullNames;
  }

  public void setCullNames(boolean cullNames) {
    this.cullNames = cullNames;
    placeNameRenderer.setCullTextEnabled(cullNames);
  }

  public final PlaceNameServiceSet getPlaceNameServiceSet() {
    return this.placeNameServiceSet;
  }

  private PriorityBlockingQueue<Runnable> getRequestQ() {
    return this.requestQ;
  }

  // ============== Tile Assembly ======================= //
  // ============== Tile Assembly ======================= //
  // ============== Tile Assembly ======================= //

  protected class NavigationTile {
    String id;
    protected PlaceNameService placeNameService;
    public Sector navSector;
    protected List<NavigationTile> subNavTiles = new ArrayList<NavigationTile>();
    private List<String> tileKeys = new ArrayList<String>();
    protected int level;

    NavigationTile(PlaceNameService placeNameService, Sector sector, int levels, String id) {
      this.placeNameService = placeNameService;
      this.id = id;
      this.navSector = sector;
      level = levels;
    }

    protected void buildSubNavTiles() {
      if (level > 0) {
        // split sector, create a navTile for each quad
        Sector[] subSectors = this.navSector.subdivide();
        for (int j = 0; j < subSectors.length; j++) {
          subNavTiles.add(
              new NavigationTile(placeNameService, subSectors[j], level - 1, this.id + "." + j));
        }
      }
    }

    public List<NavigationTile> navTilesVisible(
        DrawContext dc, double minDistSquared, double maxDistSquared) {
      ArrayList<NavigationTile> navList = new ArrayList<NavigationTile>();
      if (this.isNavSectorVisible(dc, minDistSquared, maxDistSquared)) {
        if (this.level > 0 && !this.hasSubTiles()) this.buildSubNavTiles();

        if (this.hasSubTiles()) {
          for (NavigationTile nav : subNavTiles) {
            navList.addAll(nav.navTilesVisible(dc, minDistSquared, maxDistSquared));
          }
        } else // at bottom level navigation tile
        {
          navList.add(this);
        }
      }

      return navList;
    }

    public boolean hasSubTiles() {
      return !subNavTiles.isEmpty();
    }

    protected boolean isNavSectorVisible(
        DrawContext dc, double minDistanceSquared, double maxDistanceSquared) {
      if (!navSector.intersects(dc.getVisibleSector())) return false;

      View view = dc.getView();
      Position eyePos = view.getEyePosition();
      if (eyePos == null) return false;

      // check for eyePos over globe
      if (Double.isNaN(eyePos.getLatitude().getDegrees())
          || Double.isNaN(eyePos.getLongitude().getDegrees())) return false;

      Angle lat =
          clampAngle(eyePos.getLatitude(), navSector.getMinLatitude(), navSector.getMaxLatitude());
      Angle lon =
          clampAngle(
              eyePos.getLongitude(), navSector.getMinLongitude(), navSector.getMaxLongitude());
      Vec4 p = dc.getGlobe().computePointFromPosition(lat, lon, 0d);
      double distSquared = dc.getView().getEyePoint().distanceToSquared3(p);
      //noinspection RedundantIfStatement
      if (minDistanceSquared > distSquared || maxDistanceSquared < distSquared) return false;

      return true;
    }

    public List<Tile> getTiles() {
      if (tileKeys.isEmpty()) {
        Tile[] tiles = buildTiles(this.placeNameService, this);
        // load tileKeys
        for (Tile t : tiles) {
          tileKeys.add(t.getFileCachePath());
          WorldWind.getMemoryCache(Tile.class.getName()).add(t.getFileCachePath(), t);
        }
        return Arrays.asList(tiles);
      } else {
        List<Tile> dataTiles = new ArrayList<Tile>();
        for (String s : tileKeys) {
          Tile t = (Tile) WorldWind.getMemoryCache(Tile.class.getName()).getObject(s);
          if (t != null) {
            dataTiles.add(t);
          }
        }
        return dataTiles;
      }
    }
  }

  protected static class Tile implements Cacheable {
    protected final PlaceNameService placeNameService;
    protected final Sector sector;
    protected final int row;
    protected final int column;
    private Integer hashInt = null;
    // Computed data.
    protected String fileCachePath = null;
    protected double extentVerticalExaggeration = Double.MIN_VALUE;
    protected double priority = Double.MAX_VALUE; // Default is minimum priority
    protected PlaceNameChunk dataChunk = null;

    Tile(PlaceNameService placeNameService, Sector sector, int row, int column) {
      this.placeNameService = placeNameService;
      this.sector = sector;
      this.row = row;
      this.column = column;
      this.fileCachePath = this.placeNameService.createFileCachePathFromTile(this.row, this.column);
      this.hashInt = this.computeHash();
    }

    public void setDataChunk(PlaceNameChunk chunk) {
      dataChunk = chunk;
    }

    public PlaceNameChunk getDataChunk() {
      return dataChunk;
    }

    public long getSizeInBytes() {

      long result = 32; // references

      result += this.getSector().getSizeInBytes();
      if (this.getFileCachePath() != null) result += this.getFileCachePath().length();

      if (dataChunk != null) {
        result += dataChunk.estimatedMemorySize;
      }

      return result;
    }

    static int computeRow(Angle delta, Angle latitude) {
      if (delta == null || latitude == null) {
        String msg = Logging.getMessage("nullValue.AngleIsNull");
        Logging.logger().severe(msg);
        throw new IllegalArgumentException(msg);
      }
      return (int) ((latitude.getDegrees() + 90d) / delta.getDegrees());
    }

    static int computeColumn(Angle delta, Angle longitude) {
      if (delta == null || longitude == null) {
        String msg = Logging.getMessage("nullValue.AngleIsNull");
        Logging.logger().severe(msg);
        throw new IllegalArgumentException(msg);
      }
      return (int) ((longitude.getDegrees() + 180d) / delta.getDegrees());
    }

    static Angle computeRowLatitude(int row, Angle delta) {
      if (delta == null) {
        String msg = Logging.getMessage("nullValue.AngleIsNull");
        Logging.logger().severe(msg);
        throw new IllegalArgumentException(msg);
      }
      return Angle.fromDegrees(-90d + delta.getDegrees() * row);
    }

    static Angle computeColumnLongitude(int column, Angle delta) {
      if (delta == null) {
        String msg = Logging.getMessage("nullValue.AngleIsNull");
        Logging.logger().severe(msg);
        throw new IllegalArgumentException(msg);
      }
      return Angle.fromDegrees(-180 + delta.getDegrees() * column);
    }

    public Integer getHashInt() {
      return hashInt;
    }

    int computeHash() {
      return this.getFileCachePath() != null ? this.getFileCachePath().hashCode() : 0;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      final Tile tile = (Tile) o;

      return !(this.getFileCachePath() != null
          ? !this.getFileCachePath().equals(tile.getFileCachePath())
          : tile.getFileCachePath() != null);
    }

    public String getFileCachePath() {
      if (this.fileCachePath == null)
        this.fileCachePath =
            this.placeNameService.createFileCachePathFromTile(this.row, this.column);

      return this.fileCachePath;
    }

    public PlaceNameService getPlaceNameService() {
      return placeNameService;
    }

    public java.net.URL getRequestURL() throws java.net.MalformedURLException {
      return this.placeNameService.createServiceURLFromSector(this.sector);
    }

    public Sector getSector() {
      return sector;
    }

    public int hashCode() {
      return this.hashInt;
    }

    protected boolean isTileInMemoryWithData() {
      Tile t =
          (Tile) WorldWind.getMemoryCache(Tile.class.getName()).getObject(this.getFileCachePath());
      return !(t == null || t.getDataChunk() == null);
    }

    public double getPriority() {
      return priority;
    }

    public void setPriority(double priority) {
      this.priority = priority;
    }
  }

  protected Tile[] buildTiles(PlaceNameService placeNameService, NavigationTile navTile) {
    final Angle dLat = placeNameService.getTileDelta().getLatitude();
    final Angle dLon = placeNameService.getTileDelta().getLongitude();

    // Determine the row and column offset from the global tiling origin for the southwest tile
    // corner
    int firstRow = Tile.computeRow(dLat, navTile.navSector.getMinLatitude());
    int firstCol = Tile.computeColumn(dLon, navTile.navSector.getMinLongitude());
    int lastRow = Tile.computeRow(dLat, navTile.navSector.getMaxLatitude().subtract(dLat));
    int lastCol = Tile.computeColumn(dLon, navTile.navSector.getMaxLongitude().subtract(dLon));

    int nLatTiles = lastRow - firstRow + 1;
    int nLonTiles = lastCol - firstCol + 1;

    Tile[] tiles = new Tile[nLatTiles * nLonTiles];

    Angle p1 = Tile.computeRowLatitude(firstRow, dLat);
    for (int row = 0; row <= lastRow - firstRow; row++) {
      Angle p2;
      p2 = p1.add(dLat);

      Angle t1 = Tile.computeColumnLongitude(firstCol, dLon);
      for (int col = 0; col <= lastCol - firstCol; col++) {
        Angle t2;
        t2 = t1.add(dLon);
        // Need offset row and column to correspond to total ro/col numbering
        tiles[col + row * nLonTiles] =
            new Tile(placeNameService, new Sector(p1, p2, t1, t2), row + firstRow, col + firstCol);
        t1 = t2;
      }
      p1 = p2;
    }

    return tiles;
  }

  // ============== Place Name Data Structures ======================= //
  // ============== Place Name Data Structures ======================= //
  // ============== Place Name Data Structures ======================= //

  protected static class PlaceNameChunk implements Cacheable {
    protected final PlaceNameService placeNameService;
    protected final CharBuffer textArray;
    protected final int[] textIndexArray;
    protected final double[] latlonArray;
    protected final int numEntries;
    protected final long estimatedMemorySize;

    protected PlaceNameChunk(
        PlaceNameService service,
        CharBuffer text,
        int[] textIndices,
        double[] positions,
        int numEntries) {
      this.placeNameService = service;
      this.textArray = text;
      this.textIndexArray = textIndices;
      this.latlonArray = positions;
      this.numEntries = numEntries;
      this.estimatedMemorySize = this.computeEstimatedMemorySize();
    }

    protected long computeEstimatedMemorySize() {
      long result = 0;
      if (!textArray.isDirect()) result += (Character.SIZE / 8) * textArray.capacity();
      result += (Integer.SIZE / 8) * textIndexArray.length;
      result += (Double.SIZE / 8) * latlonArray.length;
      return result;
    }

    protected Position getPosition(int index) {
      int latlonIndex = 2 * index;
      return Position.fromDegrees(latlonArray[latlonIndex], latlonArray[latlonIndex + 1], 0);
    }

    protected PlaceNameService getPlaceNameService() {
      return this.placeNameService;
    }

    protected CharSequence getText(int index) {
      int beginIndex = textIndexArray[index];
      int endIndex = (index + 1 < numEntries) ? textIndexArray[index + 1] : textArray.length();
      return this.textArray.subSequence(beginIndex, endIndex);
    }

    public long getSizeInBytes() {
      return this.estimatedMemorySize;
    }

    private Iterable<GeographicText> makeIterable(DrawContext dc) {
      // get dispay dist for this service for use in label annealing
      double maxDisplayDistance = this.getPlaceNameService().getMaxDisplayDistance();
      ArrayList<GeographicText> list = new ArrayList<GeographicText>();
      for (int i = 0; i < this.numEntries; i++) {
        CharSequence str = getText(i);
        Position pos = getPosition(i);
        GeographicText text = new UserFacingText(str, pos);
        text.setFont(this.placeNameService.getFont());
        text.setColor(this.placeNameService.getColor());
        text.setBackgroundColor(this.placeNameService.getBackgroundColor());
        text.setVisible(isNameVisible(dc, this.placeNameService, pos));
        text.setPriority(maxDisplayDistance);
        list.add(text);
      }
      return list;
    }
  }

  // ============== Rendering ======================= //
  // ============== Rendering ======================= //
  // ============== Rendering ======================= //

  private final GeographicTextRenderer placeNameRenderer = new GeographicTextRenderer();

  @Override
  protected void doRender(DrawContext dc) {
    this.referencePoint = this.computeReferencePoint(dc);

    int serviceCount = this.placeNameServiceSet.getServiceCount();
    for (int i = 0; i < serviceCount; i++) {
      PlaceNameService placeNameService = this.placeNameServiceSet.getService(i);
      if (!isServiceVisible(dc, placeNameService)) continue;

      double minDistSquared =
          placeNameService.getMinDisplayDistance() * placeNameService.getMinDisplayDistance();
      double maxDistSquared =
          placeNameService.getMaxDisplayDistance() * placeNameService.getMaxDisplayDistance();

      if (isSectorVisible(
          dc, placeNameService.getMaskingSector(), minDistSquared, maxDistSquared)) {
        ArrayList<Tile> baseTiles = new ArrayList<Tile>();
        NavigationTile navTile = this.navTiles.get(i);
        // drill down into tiles to find bottom level navTiles visible
        List<NavigationTile> list = navTile.navTilesVisible(dc, minDistSquared, maxDistSquared);
        for (NavigationTile nt : list) {
          baseTiles.addAll(nt.getTiles());
        }

        for (Tile tile : baseTiles) {
          try {
            drawOrRequestTile(dc, tile, minDistSquared, maxDistSquared);
          } catch (Exception e) {
            Logging.logger()
                .log(
                    Level.FINE,
                    Logging.getMessage("layers.PlaceNameLayer.ExceptionRenderingTile"),
                    e);
          }
        }
      }
    }

    this.sendRequests();
    this.requestQ.clear();
  }

  protected Vec4 computeReferencePoint(DrawContext dc) {
    if (dc.getViewportCenterPosition() != null)
      return dc.getGlobe().computePointFromPosition(dc.getViewportCenterPosition());

    java.awt.geom.Rectangle2D viewport = dc.getView().getViewport();
    int x = (int) viewport.getWidth() / 2;
    for (int y = (int) (0.5 * viewport.getHeight()); y >= 0; y--) {
      Position pos = dc.getView().computePositionFromScreenPoint(x, y);
      if (pos == null) continue;

      return dc.getGlobe().computePointFromPosition(pos.getLatitude(), pos.getLongitude(), 0d);
    }

    return null;
  }

  protected Vec4 getReferencePoint() {
    return this.referencePoint;
  }

  protected void drawOrRequestTile(
      DrawContext dc,
      Tile tile,
      double minDisplayDistanceSquared,
      double maxDisplayDistanceSquared) {
    if (!isTileVisible(dc, tile, minDisplayDistanceSquared, maxDisplayDistanceSquared)) return;

    if (tile.isTileInMemoryWithData()) {
      PlaceNameChunk placeNameChunk = tile.getDataChunk();
      if (placeNameChunk.numEntries > 0) {
        Iterable<GeographicText> renderIter = placeNameChunk.makeIterable(dc);
        this.placeNameRenderer.render(dc, renderIter);
      }
      return;
    }

    // Tile's data isn't available, so request it
    if (!tile.getPlaceNameService()
        .isResourceAbsent(tile.getPlaceNameService().getTileNumber(tile.row, tile.column))) {
      this.requestTile(dc, tile);
    }
  }

  protected static boolean isServiceVisible(DrawContext dc, PlaceNameService placeNameService) {
    //noinspection SimplifiableIfStatement
    if (!placeNameService.isEnabled()) return false;

    return (dc.getVisibleSector() != null)
        && placeNameService.getMaskingSector().intersects(dc.getVisibleSector());
    //
    //        return
    // placeNameService.getExtent(dc).intersects(dc.getView().getFrustumInModelCoordinates());
  }

  protected static boolean isSectorVisible(
      DrawContext dc, Sector sector, double minDistanceSquared, double maxDistanceSquared) {

    View view = dc.getView();
    Position eyePos = view.getEyePosition();
    if (eyePos == null) return false;

    Angle lat = clampAngle(eyePos.getLatitude(), sector.getMinLatitude(), sector.getMaxLatitude());
    Angle lon =
        clampAngle(eyePos.getLongitude(), sector.getMinLongitude(), sector.getMaxLongitude());
    Vec4 p = dc.getGlobe().computePointFromPosition(lat, lon, 0d);
    double distSquared = dc.getView().getEyePoint().distanceToSquared3(p);
    //noinspection RedundantIfStatement
    if (minDistanceSquared > distSquared || maxDistanceSquared < distSquared) return false;

    return true;
  }

  protected static boolean isTileVisible(
      DrawContext dc, Tile tile, double minDistanceSquared, double maxDistanceSquared) {
    if (!tile.getSector().intersects(dc.getVisibleSector())) return false;

    View view = dc.getView();
    Position eyePos = view.getEyePosition();
    if (eyePos == null) return false;

    Angle lat =
        clampAngle(
            eyePos.getLatitude(),
            tile.getSector().getMinLatitude(),
            tile.getSector().getMaxLatitude());
    Angle lon =
        clampAngle(
            eyePos.getLongitude(),
            tile.getSector().getMinLongitude(),
            tile.getSector().getMaxLongitude());
    Vec4 p = dc.getGlobe().computePointFromPosition(lat, lon, 0d);
    double distSquared = dc.getView().getEyePoint().distanceToSquared3(p);
    //noinspection RedundantIfStatement
    if (minDistanceSquared > distSquared || maxDistanceSquared < distSquared) return false;

    return true;
  }

  protected static boolean isNameVisible(
      DrawContext dc, PlaceNameService service, Position namePosition) {
    double elevation = dc.getVerticalExaggeration() * namePosition.getElevation();
    Vec4 namePoint =
        dc.getGlobe()
            .computePointFromPosition(
                namePosition.getLatitude(), namePosition.getLongitude(), elevation);
    Vec4 eyeVec = dc.getView().getEyePoint();

    double dist = eyeVec.distanceTo3(namePoint);
    return dist >= service.getMinDisplayDistance() && dist <= service.getMaxDisplayDistance();
  }

  protected static Angle clampAngle(Angle a, Angle min, Angle max) {
    double degrees = a.degrees;
    double minDegrees = min.degrees;
    double maxDegrees = max.degrees;
    return Angle.fromDegrees(
        degrees < minDegrees ? minDegrees : (degrees > maxDegrees ? maxDegrees : degrees));
  }

  // ============== Image Reading and Downloading ======================= //
  // ============== Image Reading and Downloading ======================= //
  // ============== Image Reading and Downloading ======================= //

  protected void requestTile(DrawContext dc, Tile tile) {
    Vec4 centroid = dc.getGlobe().computePointFromPosition(tile.getSector().getCentroid(), 0);
    if (this.getReferencePoint() != null)
      tile.setPriority(centroid.distanceTo3(this.getReferencePoint()));

    RequestTask task = new RequestTask(tile, this);
    this.getRequestQ().add(task);
  }

  protected void sendRequests() {
    Runnable task = this.requestQ.poll();
    while (task != null) {
      if (!WorldWind.getTaskService().isFull()) {
        WorldWind.getTaskService().addTask(task);
      }
      task = this.requestQ.poll();
    }
  }

  protected static class RequestTask implements Runnable, Comparable<RequestTask> {
    protected final PlaceNameLayer layer;
    protected final Tile tile;

    RequestTask(Tile tile, PlaceNameLayer layer) {
      this.layer = layer;
      this.tile = tile;
    }

    public void run() {
      if (this.tile.isTileInMemoryWithData()) return;

      final java.net.URL tileURL =
          this.layer.getDataFileStore().findFile(tile.getFileCachePath(), false);
      if (tileURL != null) {
        if (this.layer.loadTile(this.tile, tileURL)) {
          tile.getPlaceNameService()
              .unmarkResourceAbsent(
                  tile.getPlaceNameService().getTileNumber(tile.row, tile.column));
          this.layer.firePropertyChange(AVKey.LAYER, null, this);
          return;
        }
      }

      this.layer.downloadTile(this.tile);
    }

    /**
     * @param that the task to compare
     * @return -1 if <code>this</code> less than <code>that</code>, 1 if greater than, 0 if equal
     * @throws IllegalArgumentException if <code>that</code> is null
     */
    public int compareTo(RequestTask that) {
      if (that == null) {
        String msg = Logging.getMessage("nullValue.RequestTaskIsNull");
        Logging.logger().severe(msg);
        throw new IllegalArgumentException(msg);
      }
      return this.tile.getPriority() == that.tile.getPriority()
          ? 0
          : this.tile.getPriority() < that.tile.getPriority() ? -1 : 1;
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      final RequestTask that = (RequestTask) o;

      // Don't include layer in comparison so that requests are shared among layers
      return !(tile != null ? !tile.equals(that.tile) : that.tile != null);
    }

    public int hashCode() {
      return (tile != null ? tile.hashCode() : 0);
    }

    public String toString() {
      return this.tile.toString();
    }
  }

  protected boolean loadTile(Tile tile, java.net.URL url) {
    if (WWIO.isFileOutOfDate(url, this.placeNameServiceSet.getExpiryTime())) {
      // The file has expired. Delete it then request download of newer.
      this.getDataFileStore().removeFile(url);
      String message = Logging.getMessage("generic.DataFileExpired", url);
      Logging.logger().fine(message);
      return false;
    }

    PlaceNameChunk tileData;
    synchronized (this.fileLock) {
      tileData = readTileData(tile, url);
    }

    if (tileData == null) {
      // Assume that something's wrong with the file and delete it.
      this.getDataFileStore().removeFile(url);
      tile.getPlaceNameService()
          .markResourceAbsent(tile.getPlaceNameService().getTileNumber(tile.row, tile.column));
      String message = Logging.getMessage("generic.DeletedCorruptDataFile", url);
      Logging.logger().fine(message);
      return false;
    }

    tile.setDataChunk(tileData);
    WorldWind.getMemoryCache(Tile.class.getName()).add(tile.getFileCachePath(), tile);
    return true;
  }

  protected static PlaceNameChunk readTileData(Tile tile, java.net.URL url) {
    java.io.InputStream is = null;

    try {
      String path = url.getFile();
      path =
          path.replaceAll(
              "%20", " "); // TODO: find a better way to get a path usable by FileInputStream

      java.io.FileInputStream fis = new java.io.FileInputStream(path);
      java.io.BufferedInputStream buf = new java.io.BufferedInputStream(fis);
      is = new java.util.zip.GZIPInputStream(buf);

      GMLPlaceNameSAXHandler handler = new GMLPlaceNameSAXHandler();
      javax.xml.parsers.SAXParserFactory.newInstance().newSAXParser().parse(is, handler);
      return handler.createPlaceNameChunk(tile.getPlaceNameService());
    } catch (Exception e) {
      // todo log actual error
      Logging.logger()
          .log(
              Level.FINE,
              Logging.getMessage(
                  "layers.PlaceNameLayer.ExceptionAttemptingToReadFile", url.toString()),
              e);
    } finally {
      try {
        if (is != null) is.close();
      } catch (java.io.IOException e) {
        Logging.logger()
            .log(
                Level.FINE,
                Logging.getMessage(
                    "layers.PlaceNameLayer.ExceptionAttemptingToReadFile", url.toString()),
                e);
      }
    }

    return null;
  }

  protected static CharBuffer newCharBuffer(int numElements) {
    ByteBuffer bb = ByteBuffer.allocateDirect((Character.SIZE / 8) * numElements);
    bb.order(ByteOrder.nativeOrder());
    return bb.asCharBuffer();
  }

  protected static class GMLPlaceNameSAXHandler extends org.xml.sax.helpers.DefaultHandler {
    protected static final String GML_FEATURE_MEMBER = "gml:featureMember";
    protected static final String TOPP_FULL_NAME_ND = "topp:full_name_nd";
    protected static final String TOPP_LATITUDE = "topp:latitude";
    protected static final String TOPP_LONGITUDE = "topp:longitude";
    protected final LinkedList<String> internedQNameStack = new LinkedList<String>();
    protected boolean inBeginEndPair = false;
    protected StringBuilder latBuffer = new StringBuilder();
    protected StringBuilder lonBuffer = new StringBuilder();

    StringBuilder textArray = new StringBuilder();
    int[] textIndexArray = new int[16];
    double[] latlonArray = new double[16];
    int numEntries = 0;

    protected GMLPlaceNameSAXHandler() {}

    protected PlaceNameChunk createPlaceNameChunk(PlaceNameService service) {
      int numChars = this.textArray.length();
      CharBuffer textBuffer = newCharBuffer(numChars);
      textBuffer.put(this.textArray.toString());
      textBuffer.rewind();
      return new PlaceNameChunk(
          service, textBuffer, this.textIndexArray, this.latlonArray, this.numEntries);
    }

    protected void beginEntry() {
      int textIndex = this.textArray.length();
      this.textIndexArray = append(this.textIndexArray, this.numEntries, textIndex);
      this.inBeginEndPair = true;
    }

    protected void endEntry() {
      double lat = this.parseDouble(this.latBuffer);
      double lon = this.parseDouble(this.lonBuffer);
      int numLatLon = 2 * this.numEntries;
      this.latlonArray = this.append(this.latlonArray, numLatLon, lat);
      numLatLon++;
      this.latlonArray = this.append(this.latlonArray, numLatLon, lon);

      this.latBuffer.delete(0, this.latBuffer.length());
      this.lonBuffer.delete(0, this.lonBuffer.length());
      this.inBeginEndPair = false;
      this.numEntries++;
    }

    protected double parseDouble(StringBuilder sb) {
      double value = 0;
      try {
        value = Double.parseDouble(sb.toString());
      } catch (NumberFormatException e) {
        Logging.logger()
            .log(
                Level.FINE,
                Logging.getMessage("layers.PlaceNameLayer.ExceptionAttemptingToReadFile", ""),
                e);
      }
      return value;
    }

    protected int[] append(int[] array, int index, int value) {
      if (index >= array.length) array = this.resizeArray(array);
      array[index] = value;
      return array;
    }

    protected int[] resizeArray(int[] oldArray) {
      int newSize = 2 * oldArray.length;
      int[] newArray = new int[newSize];
      System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
      return newArray;
    }

    protected double[] append(double[] array, int index, double value) {
      if (index >= array.length) array = this.resizeArray(array);
      array[index] = value;
      return array;
    }

    protected double[] resizeArray(double[] oldArray) {
      int newSize = 2 * oldArray.length;
      double[] newArray = new double[newSize];
      System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
      return newArray;
    }

    @SuppressWarnings({"StringEquality"})
    public void characters(char ch[], int start, int length) {
      if (!this.inBeginEndPair) return;

      // Top of QName stack is an interned string,
      // so we can use pointer comparison.
      String internedTopQName = this.internedQNameStack.getFirst();

      StringBuilder sb = null;
      if (TOPP_LATITUDE == internedTopQName) sb = this.latBuffer;
      else if (TOPP_LONGITUDE == internedTopQName) sb = this.lonBuffer;
      else if (TOPP_FULL_NAME_ND == internedTopQName) sb = this.textArray;

      if (sb != null) sb.append(ch, start, length);
    }

    public void startElement(
        String uri, String localName, String qName, org.xml.sax.Attributes attributes) {
      // Don't validate uri, localName or attributes because they aren't used.
      // Intern the qName string so we can use pointer comparison.
      String internedQName = qName.intern();
      //noinspection StringEquality
      if (GML_FEATURE_MEMBER == internedQName) this.beginEntry();
      this.internedQNameStack.addFirst(internedQName);
    }

    public void endElement(String uri, String localName, String qName) {
      // Don't validate uri or localName because they aren't used.
      // Intern the qName string so we can use pointer comparison.
      String internedQName = qName.intern();
      //noinspection StringEquality
      if (GML_FEATURE_MEMBER == internedQName) this.endEntry();
      this.internedQNameStack.removeFirst();
    }
  }

  protected void downloadTile(final Tile tile) {
    downloadTile(tile, null);
  }

  protected void downloadTile(final Tile tile, DownloadPostProcessor postProcessor) {
    if (!this.isNetworkRetrievalEnabled()) return;

    if (!WorldWind.getRetrievalService().isAvailable()) return;

    java.net.URL url;
    try {
      url = tile.getRequestURL();
      if (WorldWind.getNetworkStatus().isHostUnavailable(url)) return;
    } catch (java.net.MalformedURLException e) {
      Logging.logger()
          .log(
              java.util.logging.Level.SEVERE,
              Logging.getMessage("layers.PlaceNameLayer.ExceptionCreatingUrl", tile),
              e);
      return;
    }

    Retriever retriever;

    if ("http".equalsIgnoreCase(url.getProtocol()) || "https".equalsIgnoreCase(url.getProtocol())) {
      if (postProcessor == null) postProcessor = new DownloadPostProcessor(this, tile);
      retriever = new HTTPRetriever(url, postProcessor);
    } else {
      Logging.logger()
          .severe(
              Logging.getMessage("layers.PlaceNameLayer.UnknownRetrievalProtocol", url.toString()));
      return;
    }

    // Apply any overridden timeouts.
    Integer cto = AVListImpl.getIntegerValue(this, AVKey.URL_CONNECT_TIMEOUT);
    if (cto != null && cto > 0) retriever.setConnectTimeout(cto);
    Integer cro = AVListImpl.getIntegerValue(this, AVKey.URL_READ_TIMEOUT);
    if (cro != null && cro > 0) retriever.setReadTimeout(cro);
    Integer srl = AVListImpl.getIntegerValue(this, AVKey.RETRIEVAL_QUEUE_STALE_REQUEST_LIMIT);
    if (srl != null && srl > 0) retriever.setStaleRequestLimit(srl);

    WorldWind.getRetrievalService().runRetriever(retriever, tile.getPriority());
  }

  protected void saveBuffer(java.nio.ByteBuffer buffer, java.io.File outFile)
      throws java.io.IOException {
    synchronized (this.fileLock) // sychronized with read of file in RequestTask.run()
    {
      WWIO.saveBuffer(buffer, outFile);
    }
  }

  protected static class DownloadPostProcessor extends AbstractRetrievalPostProcessor {
    protected final PlaceNameLayer layer;
    protected final Tile tile;
    protected final FileStore fileStore;

    public DownloadPostProcessor(PlaceNameLayer layer, Tile tile) {
      // No arg check; the class has protected access.
      this(layer, tile, null);
    }

    public DownloadPostProcessor(PlaceNameLayer layer, Tile tile, FileStore fileStore) {
      // No arg check; the class has protected access.

      //noinspection RedundantCast
      super((AVList) layer);

      this.layer = layer;
      this.tile = tile;
      this.fileStore = fileStore;
    }

    protected FileStore getFileStore() {
      return this.fileStore != null ? this.fileStore : this.layer.getDataFileStore();
    }

    @Override
    protected void markResourceAbsent() {
      this.tile
          .getPlaceNameService()
          .markResourceAbsent(
              this.tile.getPlaceNameService().getTileNumber(this.tile.row, this.tile.column));
    }

    @Override
    protected Object getFileLock() {
      return this.layer.fileLock;
    }

    protected File doGetOutputFile() {
      return this.getFileStore().newFile(this.tile.getFileCachePath());
    }

    @Override
    protected ByteBuffer handleSuccessfulRetrieval() {
      ByteBuffer buffer = super.handleSuccessfulRetrieval();

      if (buffer != null) {
        // Fire a property change to denote that the layer's backing data has changed.
        this.layer.firePropertyChange(AVKey.LAYER, null, this);
      }

      return buffer;
    }

    @Override
    protected ByteBuffer handleXMLContent() throws IOException {
      // Check for an exception report
      String s = WWIO.byteBufferToString(this.getRetriever().getBuffer(), 1024, null);
      if (s.contains("<ExceptionReport>")) {
        // TODO: Parse the xml and include only the message text in the log message.

        StringBuilder sb = new StringBuilder(this.getRetriever().getName());

        sb.append("\n");
        sb.append(WWIO.byteBufferToString(this.getRetriever().getBuffer(), 2048, null));
        Logging.logger().warning(sb.toString());

        return null;
      }

      this.saveBuffer();

      return this.getRetriever().getBuffer();
    }
  }

  // *** Bulk download ***
  // *** Bulk download ***
  // *** Bulk download ***

  /**
   * Start a new {@link BulkRetrievalThread} that downloads all placenames for a given sector and
   * resolution to the current World Wind file cache.
   *
   * <p>This method creates and starts a thread to perform the download. A reference to the thread
   * is returned. To create a downloader that has not been started, construct a {@link
   * PlaceNameLayerBulkDownloader}.
   *
   * <p>Note that the target resolution must be provided in radians of latitude per texel, which is
   * the resolution in meters divided by the globe radius.
   *
   * @param sector the sector to download data for.
   * @param resolution the target resolution, provided in radians of latitude per texel.
   * @param listener an optional retrieval listener. May be null.
   * @return the {@link PlaceNameLayerBulkDownloader} that executes the retrieval.
   * @throws IllegalArgumentException if the sector is null or the resolution is less than zero.
   * @see PlaceNameLayerBulkDownloader
   */
  public BulkRetrievalThread makeLocal(
      Sector sector, double resolution, BulkRetrievalListener listener) {
    PlaceNameLayerBulkDownloader thread =
        new PlaceNameLayerBulkDownloader(
            this, sector, resolution, this.getDataFileStore(), listener);
    thread.setDaemon(true);
    thread.start();
    return thread;
  }

  /**
   * Start a new {@link BulkRetrievalThread} that downloads all placenames for a given sector and
   * resolution to a specified file store.
   *
   * <p>This method creates and starts a thread to perform the download. A reference to the thread
   * is returned. To create a downloader that has not been started, construct a {@link
   * PlaceNameLayerBulkDownloader}.
   *
   * <p>Note that the target resolution must be provided in radians of latitude per texel, which is
   * the resolution in meters divided by the globe radius.
   *
   * @param sector the sector to download data for.
   * @param resolution the target resolution, provided in radians of latitude per texel.
   * @param fileStore the file store in which to place the downloaded elevations. If null the
   *     current World Wind file cache is used.
   * @param listener an optional retrieval listener. May be null.
   * @return the {@link PlaceNameLayerBulkDownloader} that executes the retrieval.
   * @throws IllegalArgumentException if the sector is null or the resolution is less than zero.
   * @see PlaceNameLayerBulkDownloader
   */
  public BulkRetrievalThread makeLocal(
      Sector sector, double resolution, FileStore fileStore, BulkRetrievalListener listener) {
    PlaceNameLayerBulkDownloader thread =
        new PlaceNameLayerBulkDownloader(this, sector, resolution, fileStore, listener);
    thread.setDaemon(true);
    thread.start();
    return thread;
  }

  /**
   * Get the estimated size in bytes of the placenames not in the World Wind file cache for the
   * given sector and resolution.
   *
   * <p>Note that the target resolution must be provided in radians of latitude per texel, which is
   * the resolution in meters divided by the globe radius.
   *
   * @param sector the sector to estimate.
   * @param resolution the target resolution, provided in radians of latitude per texel.
   * @return the estimated size in bytes of the missing placenames.
   * @throws IllegalArgumentException if the sector is null or the resolution is less than zero.
   */
  public long getEstimatedMissingDataSize(Sector sector, double resolution) {
    return this.getEstimatedMissingDataSize(sector, resolution, this.getDataFileStore());
  }

  /**
   * Get the estimated size in bytes of the placenames not in a specified file store for the given
   * sector and resolution.
   *
   * <p>Note that the target resolution must be provided in radians of latitude per texel, which is
   * the resolution in meters divided by the globe radius.
   *
   * @param sector the sector to estimate.
   * @param resolution the target resolution, provided in radians of latitude per texel.
   * @param fileStore the file store to examine. If null the current World Wind file cache is used.
   * @return the estimated size in byte of the missing placenames.
   * @throws IllegalArgumentException if the sector is null or the resolution is less than zero.
   */
  public long getEstimatedMissingDataSize(Sector sector, double resolution, FileStore fileStore) {
    try {
      PlaceNameLayerBulkDownloader downloader =
          new PlaceNameLayerBulkDownloader(
              this,
              sector,
              resolution,
              fileStore != null ? fileStore : this.getDataFileStore(),
              null);
      return downloader.getEstimatedMissingDataSize();
    } catch (Exception e) {
      String message =
          Logging.getMessage("generic.ExceptionDuringDataSizeEstimate", this.getName());
      Logging.logger().severe(message);
      throw new RuntimeException(message);
    }
  }
}