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 static class AppFrame extends ApplicationTemplate.AppFrame {
    public AppFrame() {
      super(true, true, true);

      RenderableLayer layer = new RenderableLayer();
      layer.setName("Video on terrain");
      insertBeforePlacenames(this.getWwd(), layer);
      this.layerPanel.update(
          this.getWwd()); // makes the ApplicationTemplate layer list show the new layer

      // Set up a SelectListener to drag the SurfaceImage.
      this.getWwd().addSelectListener(new SurfaceImageDragger(this.getWwd()));

      final SurfaceImage surfaceImage = new SurfaceImage(makeImage(), CORNERS);
      surfaceImage.setOpacity(IMAGE_OPACITY);
      layer.addRenderable(surfaceImage);

      javax.swing.Timer timer =
          new javax.swing.Timer(
              50,
              new ActionListener() {
                public void actionPerformed(ActionEvent actionEvent) {
                  Iterable<LatLon> corners = surfaceImage.getCorners();
                  surfaceImage.setImageSource(makeImage(), corners);
                  getWwd().redraw();
                }
              });
      timer.start();
    }

    protected long counter;
    protected long start = System.currentTimeMillis();

    protected BufferedImage makeImage() {
      BufferedImage image =
          new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
      Graphics2D g = image.createGraphics();

      g.setPaint(Color.WHITE);
      g.fill3DRect(0, 0, IMAGE_SIZE, IMAGE_SIZE, false);

      g.setPaint(Color.RED);
      g.setFont(Font.decode("ARIAL-BOLD-50"));

      g.drawString(Long.toString(++this.counter) + " frames", 10, IMAGE_SIZE / 4);
      g.drawString(
          Long.toString((System.currentTimeMillis() - start) / 1000) + " sec", 10, IMAGE_SIZE / 2);
      g.drawString(
          "Heap:" + Long.toString(Runtime.getRuntime().totalMemory()), 10, 3 * IMAGE_SIZE / 4);

      g.dispose();

      return image;
    }
  }
  /**
   * Initiates a retrieval of the model referenced by this placemark. Once the resource is retrieved
   * and loaded, this calls <code>{@link #setColladaRoot(ColladaRoot)}</code> to specify this link's
   * new network resource, and sends an <code>
   * {@link gov.nasa.worldwind.avlist.AVKey#RETRIEVAL_STATE_SUCCESSFUL}</code> property change event
   * to this link's property change listeners.
   *
   * <p>This does nothing if this <code>KMLNetworkLink</code> has no <code>KMLLink</code>.
   *
   * @param address the address of the resource to retrieve
   */
  protected void retrieveModel(String address) throws IOException, XMLStreamException {
    Object o = this.parent.getRoot().resolveReference(address);
    if (o == null) return;

    ColladaRoot root = ColladaRoot.createAndParse(o);
    if (root == null) return;

    this.setColladaRoot(root);
    this.resourceRetrievalTime.set(System.currentTimeMillis());
    this.parent.getRoot().requestRedraw();
  }
    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);
      }
    }
  @SuppressWarnings({"ResultOfMethodCallIgnored"})
  protected static void markFileUsed(java.io.File file) {
    if (file == null) return;

    long currentTime = System.currentTimeMillis();

    if (file.canWrite()) file.setLastModified(currentTime);

    if (file.isDirectory()) return;

    java.io.File parent = file.getParentFile();
    if (parent != null && parent.canWrite()) parent.setLastModified(currentTime);
  }
    /** 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);
              }
            }
          });
    }
    protected BufferedImage makeImage() {
      BufferedImage image =
          new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
      Graphics2D g = image.createGraphics();

      g.setPaint(Color.WHITE);
      g.fill3DRect(0, 0, IMAGE_SIZE, IMAGE_SIZE, false);

      g.setPaint(Color.RED);
      g.setFont(Font.decode("ARIAL-BOLD-50"));

      g.drawString(Long.toString(++this.counter) + " frames", 10, IMAGE_SIZE / 4);
      g.drawString(
          Long.toString((System.currentTimeMillis() - start) / 1000) + " sec", 10, IMAGE_SIZE / 2);
      g.drawString(
          "Heap:" + Long.toString(Runtime.getRuntime().totalMemory()), 10, 3 * IMAGE_SIZE / 4);

      g.dispose();

      return image;
    }
  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);
    }
  }