Skip To Content ArcGIS for Developers Sign In Dashboard

ArcGIS Runtime SDK for Java

Message in a bottle

Download Sample Viewer

Description

This application uses an online Geoprocessing service to compute the path taken by a bottle floating in the sea. It uses a Geoprocessor to call a MessageInABottle geoprocessing service. To use the sample, specify the number of days and click a point in the ocean. The path of a bottle dropped at the click point over the specified number of days will be drawn on the map. In the code-behind, a Geoprocessor is used to pass the number of days and the clicked point to the geoprocessing service. When the results are returned, they are added to a GraphicsLayer in the map.

Code snippet


    // create a geoprocessor
    Geoprocessor geoprocessor = new Geoprocessor(URL_GEOPROCESSING_SERVICE);
 
    // provide input arguments
    List<GPParameter> gpInputParams = new ArrayList<GPParameter>(); 
    gpInputParams.add(<appropriate instance of GPParameter>);
    // repeat for each input parameter
    
    // run geoprocessing task and process result
    // one of the options to run the geoprocessing task
    geoprocessor.executeAsync(...);
  

Sample Code

/* Copyright 2014 Esri

All rights reserved under the copyright laws of the United States
and applicable international laws, treaties, and conventions.

You may freely redistribute and use this sample code, with or
without modification, provided you include the original copyright
notice and use restrictions.

See the use restrictions.*/
package com.esri.client.samples.geoprocessing;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.border.LineBorder;
import javax.swing.event.MouseInputAdapter;

import com.esri.core.geometry.Envelope;
import com.esri.core.geometry.Geometry;
import com.esri.core.geometry.GeometryEngine;
import com.esri.core.geometry.Point;
import com.esri.core.geometry.SpatialReference;
import com.esri.core.map.CallbackListener;
import com.esri.core.map.Graphic;
import com.esri.core.symbol.SimpleLineSymbol;
import com.esri.core.symbol.SimpleMarkerSymbol;
import com.esri.core.symbol.SimpleMarkerSymbol.Style;
import com.esri.core.tasks.ags.geoprocessing.GPDouble;
import com.esri.core.tasks.ags.geoprocessing.GPFeatureRecordSetLayer;
import com.esri.core.tasks.ags.geoprocessing.GPParameter;
import com.esri.core.tasks.ags.geoprocessing.Geoprocessor;
import com.esri.map.ArcGISTiledMapServiceLayer;
import com.esri.map.GraphicsLayer;
import com.esri.map.JMap;

/**
 * This sample demonstrates use of the {@link Geoprocessor} to call an online MessageInABottle 
 * geoprocessing service. To use the sample, specify the number of days and click a point in 
 * the ocean. The path of a bottle dropped at the click point over the specified number of days 
 * will be drawn on the map.
 * <p>
 * In the code, a {@link Geoprocessor} is used to pass the number of days and the clicked point 
 * to the geoprocessing service. When the results are returned, they are added to a
 * {@link GraphicsLayer} in the map.
 */
public class MessageInABottleApp {

  // resources
  private final String URL_OCEANS_BASEMAP =
      "http://server.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer";

  private JMap map;
  private JComponent contentPane;
  private JProgressBar progressBar;
  private AtomicInteger tasksInProgress = new AtomicInteger(0);

  // ------------------------------------------------------------------------
  // Constructor
  // ------------------------------------------------------------------------
  public MessageInABottleApp() {
  }

  // ------------------------------------------------------------------------
  // Core functionality
  // ------------------------------------------------------------------------
  /**
   * Class that executes a remote geoprocessing service to calculate a bottle's path.
   */
  class BottlePathExecutor extends MouseInputAdapter {

    // URL to remote geoprocessing service
    private final String URL_GEOPROCESSING_SERVICE =
        "http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Specialty/" +
            "ESRI_Currents_World/GPServer/MessageInABottle";

    // symbology
    private final SimpleMarkerSymbol SYM_BOTTLE_START_POINT =
        new SimpleMarkerSymbol(Color.BLACK, 18, Style.X);
    private final SimpleLineSymbol SYM_BOTTLE_PATH =
        new SimpleLineSymbol(Color.RED, 2);

    JMap jMap;
    GraphicsLayer graphicsLayer;
    JTextFieldInt txtDays;

    /**
     * Creates an object that executes the geoprocessing service to calculate a bottle's path.
     * @param jMap map to get the starting point, and to convert between spatial coordinates.
     * @param graphicsLayer graphics layer to which the result bottle path will be added.
     * @param txtDays input to the geoprocessing service indicating the number of days the
     * bottle travels from its starting point.
     */
    public BottlePathExecutor(JMap jMap, GraphicsLayer graphicsLayer, JTextFieldInt txtDays) {
      this.jMap = jMap;
      this.graphicsLayer = graphicsLayer;
      this.txtDays = txtDays;

      SYM_BOTTLE_PATH.setStyle(SimpleLineSymbol.Style.DASH_DOT_DOT);
    }

    /**
     * Computes the bottle path on click of the mouse.
     */
    @Override
    public void mouseClicked(MouseEvent mouseEvent) {

      super.mouseClicked(mouseEvent);

      if (mouseEvent.getButton() == MouseEvent.BUTTON3) {
        graphicsLayer.removeAll();
        return;
      }

      tasksInProgress.incrementAndGet();
      updateProgressBarUI("Computing the path of the bottle...", true);

      // the click point is the starting point
      Point startPoint = jMap.toMapPoint(mouseEvent.getX(), mouseEvent.getY());
      Graphic startPointGraphic = new Graphic(startPoint, SYM_BOTTLE_START_POINT);
      graphicsLayer.addGraphic(startPointGraphic);

      // execute the buffer and display the buffer zone
      executeTool(startPoint);
    }

    private void executeTool(Point startPoint) {

      // create a Geoprocessor that points to the remote geoprocessing service URL
      Geoprocessor geoprocessor = new Geoprocessor(URL_GEOPROCESSING_SERVICE);
      List<GPParameter> gpInputParams = new ArrayList<>();

      // initialize the required input parameters: refer to help link in the
      // geoprocessing service URL for a list of required parameters
      GPFeatureRecordSetLayer gpInputStartPoint = new GPFeatureRecordSetLayer("Input_Point");

      // since the base layer and geoprocessing service use different spatial references,
      // convert the input in one spatial reference to the one required by the
      // geoprocessing service.
      Geometry inputStartPoint = GeometryEngine.project(
          startPoint,
          jMap.getSpatialReference(),
          SpatialReference.create(4326));
      Graphic inputStartPointGraphic = new Graphic(inputStartPoint, SYM_BOTTLE_START_POINT);
      gpInputStartPoint.addGraphic(inputStartPointGraphic);

      GPDouble gpInputNumDays = new GPDouble("Days");
      gpInputNumDays.setValue(txtDays.getIntValue());

      gpInputParams.add(gpInputStartPoint);
      gpInputParams.add(gpInputNumDays);

      // execute the geoprocessing request

      // -------------------------------------------------------------------------------------
      // Option 1 - execute (synchronous)
      // Client execution will happen in this thread.
      // This is ideal for jobs that take less processing time and whose result size is small.
      // This will freeze the UI.
      // -------------------------------------------------------------------------------------

      /*GPParameter[] result = geoprocessor.execute(gpInputParams);
            processResult(result);*/

      // -------------------------------------------------------------------------------------
      // Option 2 - executeAsync (asynchronous)
      // Client execution and result collection will happen in that thread.
      //
      // This is ideal for jobs that take relatively more processing time than option 1
      // and whose result size is small.
      // -------------------------------------------------------------------------------------

      geoprocessor.executeAsync(
          gpInputParams,
          new CallbackListener<GPParameter[]>() {

            @Override
            public void onError(Throwable th) {
              th.printStackTrace();
            }

            @Override
            public void onCallback(final GPParameter[] result) {
              updateProgressBarUI(null, tasksInProgress.decrementAndGet() > 0);
              if (result == null || result.length == 0) {
                JOptionPane.showMessageDialog(contentPane.getParent(),
                    wrap("There was an error executing the task. " +
                        "Please ensure the clicked point is in an ocean and not on land."),
                        "",
                        JOptionPane.ERROR_MESSAGE);
              } else {
                processResult(result);
              }
            }
          }
          );
    }

    /**
     * Process result from geoprocessing execution.
     * @param result output of geoprocessing execution.
     */
    private void processResult(GPParameter[] result) {
      for (GPParameter outputParameter : result) {
        if (outputParameter instanceof GPFeatureRecordSetLayer) {
          GPFeatureRecordSetLayer gpLayer = (GPFeatureRecordSetLayer) outputParameter;

          // get all the graphics and add them to the graphics layer.
          for (Graphic resultGraphic : gpLayer.getGraphics()) {

            // since the spatial references used by geoprocessing service is
            // different from the one used by map, convert one to the other.
            Geometry resultGeometry = GeometryEngine.project(
                resultGraphic.getGeometry(),
                gpLayer.getSpatialReference(),
                jMap.getSpatialReference());

            Graphic theGraphic = new Graphic(resultGeometry,SYM_BOTTLE_PATH);

            // add to the graphics layer
            graphicsLayer.addGraphic(theGraphic);
          }
        }
      }
    }
  }

  // ------------------------------------------------------------------------
  // Static methods
  // ------------------------------------------------------------------------
  /**
   * Starting point of this application.
   * @param args arguments to this application.
   */
  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        try {
          // instance of this application
          MessageInABottleApp messageInABottleApp = new MessageInABottleApp();

          // create the UI, including the map, for the application
          JFrame appWindow = messageInABottleApp.createWindow();
          appWindow.add(messageInABottleApp.createUI());
          appWindow.setVisible(true);
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    });
  }

  // ------------------------------------------------------------------------
  // Public methods
  // ------------------------------------------------------------------------
  /**
   * Creates and displays the UI, including the map, for this application.
   * @return the UI for this sample.
   */
  public JComponent createUI() {
    // application content
    contentPane = createContentPane();

    // progress bar
    progressBar = createProgressBar(contentPane);

    // description
    JTextArea description = createDescription();

    // label to input number of days
    JLabel lblDays = new JLabel("Days: ");
    lblDays.setForeground(Color.BLACK);
    lblDays.setFont(new Font("Verdana", Font.BOLD, 12));
    lblDays.setMaximumSize(new Dimension(60, 20));

    // text field to input number of days
    JTextFieldInt txtDays = new JTextFieldInt(365, 3, 365);
    txtDays.setMaximumSize(new Dimension(30, 20));

    // group the above UI items into a panel
    final JPanel controlPanel = new JPanel();
    BoxLayout boxLayout = new BoxLayout(controlPanel, BoxLayout.X_AXIS);
    controlPanel.setLayout(boxLayout);
    controlPanel.setSize(220, 20);
    controlPanel.setBorder(BorderFactory.createEmptyBorder(2,5,2,5));
    controlPanel.setBackground(new Color(255, 255, 255, 255));
    controlPanel.add(lblDays);
    controlPanel.add(txtDays);

    JPanel panel = new JPanel();
    panel.setLayout(new BorderLayout(0, 0));
    panel.setLocation(10, 10);
    panel.setSize(220, 140);
    panel.setBackground(new Color(0, 0, 0, 0));
    panel.setBorder(new LineBorder(Color.BLACK, 3, false));

    // group control and description in a panel
    panel.add(description, BorderLayout.CENTER);
    panel.add(controlPanel, BorderLayout.SOUTH);

    // map
    map = createMap(txtDays);

    contentPane.add(panel);
    contentPane.add(progressBar);
    contentPane.add(map);

    return contentPane;
  }

  // ------------------------------------------------------------------------
  // Private methods
  // ------------------------------------------------------------------------
  /**
   * Creates a window.
   * @return a window.
   */
  private JFrame createWindow() {
    JFrame window = new JFrame("Message In A Bottle Application");
    window.setBounds(100, 100, 1000, 700);
    window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    window.getContentPane().setLayout(new BorderLayout(0, 0));
    window.addWindowListener(new WindowAdapter() {
      @Override
      public void windowClosing(WindowEvent windowEvent) {
        super.windowClosing(windowEvent);
        map.dispose();
      }
    });
    return window;
  }

  /**
   * Creates a content pane.
   * @return a content pane.
   */
  private static JLayeredPane createContentPane() {
    JLayeredPane contentPane = new JLayeredPane();
    contentPane.setBounds(100, 100, 1000, 700);
    contentPane.setLayout(new BorderLayout(0, 0));
    contentPane.setVisible(true);
    return contentPane;
  }

  /**
   * Creates a progress bar.
   * @param parent progress bar's parent. The horizontal axis of the progress bar will be
   * center-aligned to the parent.
   * @return a progress bar.
   */
  private static JProgressBar createProgressBar(final JComponent parent) {
    final JProgressBar progressBar = new JProgressBar();
    progressBar.setSize(260, 20);
    parent.addComponentListener(new ComponentAdapter() {
      @Override
      public void componentResized(ComponentEvent e) {
        progressBar.setLocation(
            parent.getWidth()/2 - progressBar.getWidth()/2,
            parent.getHeight() - progressBar.getHeight() - 20);
      }
    });
    progressBar.setStringPainted(true);
    progressBar.setIndeterminate(true);
    progressBar.setVisible(false);
    return progressBar;
  }

  /**
   * Updates progress bar UI from Swing's Event Dispatch Thread.
   * @param str string to be set.
   * @param visible flag to indicate visibility of the progress bar.
   */
  private void updateProgressBarUI(final String str, final boolean visible) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        if (str != null) {
          progressBar.setString(str);
        }
        progressBar.setVisible(visible);
      }
    }); 
  }

  /**
   * Creates a map.
   * @param txtDays UI input to get number of days.
   * @return a map.
   */
  private JMap createMap(JTextFieldInt txtDays) {
    final JMap jMap = new JMap();
    // focus on Atlantic Ocean
    jMap.setExtent(new Envelope(-9942260, -210478, -334434, 6198000));

    // -----------------------------------------------------------------------------------------
    // remote map service layer - base layer with US topology
    // -----------------------------------------------------------------------------------------
    ArcGISTiledMapServiceLayer baseLayer = new ArcGISTiledMapServiceLayer(URL_OCEANS_BASEMAP);
    jMap.getLayers().add(baseLayer);

    // -----------------------------------------------------------------------------------------
    // graphics layer - to add result from the geoprocessing execution
    // -----------------------------------------------------------------------------------------
    GraphicsLayer graphicsLayer = new GraphicsLayer();
    jMap.getLayers().add(graphicsLayer);

    // -----------------------------------------------------------------------------------------
    // geoprocessing service executor
    // -----------------------------------------------------------------------------------------
    BottlePathExecutor bottlePathExecutor =
        new BottlePathExecutor(jMap, graphicsLayer, txtDays);
    jMap.addMouseListener(bottlePathExecutor);

    return jMap;
  }

  /**
   * Creates description UI component.
   * @return description.
   */
  private JTextArea createDescription() {
    JTextArea description = new JTextArea(
        "Left click at a point on an ocean to calculate the path a bottle would follow " +
            "from that point for the specified number of days, based on ocean currents. " +
        "Right click to remove graphics.");
    description.setFont(new Font("Verdana", Font.PLAIN, 11));
    description.setForeground(Color.WHITE);
    description.setBackground(new Color(0, 0, 0, 180));
    description.setEditable(false);
    description.setLineWrap(true);
    description.setWrapStyleWord(true);
    description.setBorder(BorderFactory.createEmptyBorder(5,10,5,5));
    return description;
  }
  
  private String wrap(String str) {
    // create a HTML string that wraps text when longer
    return "<html><p style='width:200px;'>" + str + "</html>";
  }

  /**
   * Text field that allows digits only.
   */
  class JTextFieldInt extends JTextField {

    private static final long serialVersionUID = 1L;

    int maxDigits;
    int defaultValue;

    /**
     * Creates a text field that accepts digits only.
     * @param num initial value.
     * @param maxDigits maximum number of digits to be allowed in its input.
     * @param defaultValue value to be returned by {@link #getIntValue()}
     * when the text field is empty.
     */
    public JTextFieldInt(int num, int maxDigits, int defaultValue) {
      super(num + "");
      this.maxDigits = maxDigits;
      this.defaultValue = defaultValue;
    }

    /**
     * Processes the input key event.
     * @param ev input key event.
     */
    @Override
    public void processKeyEvent(KeyEvent ev) {
      if (getText() != null && getText().length() > maxDigits) {
        // if current key will exceed limit,
        // then allow only special keys such as delete, backspace
        if (ev.getKeyCode() == KeyEvent.VK_DELETE ||
            ev.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
          super.processKeyEvent(ev);
        }
      } else {
        // if length is within limit, then allow digits and delete, backspace
        if (Character.isDigit(ev.getKeyChar()) ||
            ev.getKeyCode() == KeyEvent.VK_DELETE ||
            ev.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
          super.processKeyEvent(ev);
        }
      }
    }

    /**
     * Returns the corresponding integer value in the text box.
     *
     * @throws NumberFormatException if the string does not contain a parsable integer.
     * @return the corresponding integer value in the text box. Returns the default value
     * initialized in the constructor when the text field is empty.
     */
    public int getIntValue() {
      if (getText() == null) {
        return defaultValue;
      }
      return Integer.parseInt(getText());
    }
  }
}
Feedback on this topic?