介绍

本系列教程将向您展示如何使用OpenMap GIS Java Swing库构建Java应用程序。

OpenMap的开发人员指南是非常有用的文档,描述了OpenMap的体系结构,但没有说明如何逐步启动和构建应用程序。 源代码附带的示例很有用,但还不够。

OpenMap是用Swing编写的。 在撰写本文时,最新版本是5.1.12。 您可以从GitHub下载源代码和可执行jar。 将其复制/解压缩/克隆到目录后,可以通过运行适用于您平台的相关脚本( openmap.batopenmap )或双击lib/openmap.jar来执行它。 您应该看到一个完整的GIS应用程序,如图1所示。在本系列文章的最后,我们将尝试构建一个类似的应用程序。 OpenMap源代码还包含一些有关如何使用OpenMap的示例。 在本教程中,我们将基于com.bbn.openmap.app.example.SimpleMap 在第二个教程中,我们将使用com.bbn.openmap.app.example.SimpleMap2代码。 以后的教程将基于其他示例。

在本系列教程中,我们将使用最新的NetBeans IDE 8.1创建我们的应用程序。

教程1 –构建基本的地图应用程序

创建一个JFrame应用程序

在第一个教程中,我们将构建一个包含映射的基本JFrame应用程序(参见图2)。 通过执行以下步骤,打开NetBeans并创建一个新的Java应用程序:

  1. 打开菜单File→New Project,然后选择CategoryJavaProjectJava Application (图3)。 单击下一步
  2. 在下一步中,提供名称和位置。 确保对库使用专用文件夹,并且不要选择主类(图4)。 点击完成
  3. 创建新项目后,右键单击Source Packages,然后从弹出菜单中选择New→Java Package ,以创建一个名为openmap的新软件包。
  4. 右键单击“ 库”文件夹,然后从弹出菜单中选择“ 添加JAR /文件夹 ”操作。 导航到OpenMap安装的lib文件夹,然后选择openmap.jar 您可以使用相对路径,也可以将其更好地复制到Libraries文件夹中(图5)。 单击“ 打开”关闭对话框!

    OpenMap教程–第1部分

    图1:OpenMap GIS应用程序窗口

  5. 您还需要复制地图文件。 最常见的格式是.shp (ESRI Shape)。 通过在NetBeans中选择“ 文件”窗口,右键单击OpenMap1项目,然后从弹出菜单中选择“ 新建”→“文件夹” ,创建一个新的文件夹层次结构resources/map 输入名称resources ,然后单击确定 右键单击resources文件夹,然后重复该过程以在其中创建地图文件夹。 share/data/shape文件夹从您的OpenMap安装复制到map文件夹
  6. 右键单击openmap包,然后从弹出菜单中选择New→JFrame Form ,以创建新的JFrame表单。 给它起一个名字,例如MapFrame ,然后单击Finish

    OpenMap教程–第1部分

    基本的OpenMap Swing应用程序

  7. 单击Source按钮以查看生成的代码(请参见清单1)。
  8. 添加行super("Simple Map"); 在构造函数中设置窗口标题。
  9. 构造函数初始化JFrame 到目前为止,没有添加任何内容。 由于它是一个GUI应用程序,因此需要在EDT线程中运行,这就是NetBeans在main()方法中为我们编写的内容。
  10. 单击返回到“ 设计”按钮以查看空白表格。

我们可以将OpenMap JavaBeans添加到面板中。 要做到这一点:

  1. 右键单击调色板,然后选择“ 调色板管理器”
  2. 单击“ 新建类别”,然后输入OpenMap作为类别名称。 单击单击从JAR添加按钮,导航到openmap.jar 选择显示所有JavaBeans单选按钮并选择所有可用组件。 单击下一步。
  3. 选择OpenMap调色板类别,然后单击Finish 新的调色板类别已添加到“调色板”中。

    OpenMap教程–第1部分

    3:创建一个新的Java应用程序

添加地图

  1. 找到MapBean并将其拖动到MapFrame
  2. 在NetBeans的Navigator窗口中,右键单击mapBean1 ,选择Change Variable Name并将其设置为mapBean
  3. 在“ 导航器”窗口中,右键单击JFrame并将其布局更改为BorderLayout
  4. 结果代码如清单2所示。

    OpenMap教程–第1部分

    F图4:提供项目名称和位置


OpenMap教程–第1部分

图5:将openmap.jar添加到您的Libraries文件夹

com.bbn.openmap.MapBean组件是OpenMap工具包中的主要地图窗口组件。 MapBean派生自java.awt.Container类。 因为它是Swing组件,所以可以像其他任何用户界面组件一样将其添加到Java窗口层次结构中。

为了在MapBean创建地图,需要将Layers (com.bbn.openmap.Layer)添加到MapBean 图层派生自java.awt.Component ,它们是可以添加到MapBean的唯一组件。 因为LayersMapBean容器中包含的Components ,所以Layers在地图上的呈现由Java组件呈现机制控制。 该机制控制分层组件如何在彼此之上绘制。 为了确保按照正确的顺序将每个组件绘制到窗口中, Component类包括一个方法,该方法允许其告知渲染机制它想被绘制。 此功能允许Layers彼此独立工作,并让MapBean避免知道图层上正在发生的事情。

清单1:基本的Swing应用程序

public class MapFrame extends javax.swing.JFrame {

   /** Creates new form MapFrame */
   public MapFrame() {
      super("Simple Map");
      initComponents();
   }

  @SuppressWarnings("unchecked")
   // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
   private void initComponents() {
     // Content suppressed
   }  

   /**
    * @param args the command line arguments
    */
   public static void main(String args[]) {
      
      /* Create and display the form */
      java.awt.EventQueue.invokeLater(new Runnable() {
         @Override
         public void run() {
            new MapFrame().setVisible(true);
         }
      });
   }
}

清单2:添加一个MapBean

  @SuppressWarnings("unchecked")
   // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
   private void initComponents() {
     mapBean = new com.bbn.openmap.MapBean();
 setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
      getContentPane().add(mapBean, java.awt.BorderLayout.CENTER);

      pack();
   }  

清单3:将ShapeLayer添加到MapBean

/**
  * Create a ShapeLayer to show world political boundaries. Set the properties of the layer. This assumes that
  * the datafiles {@code dcwpo-browse.shp} and {@code dcwpo-browse.ssx} are in a path specified in the CLASSPATH variable.
  * These files are distributed with OpenMap and reside in the toplevel "share" subdirectory.
  */
private void initMap() {
      Properties shapeLayerProps = new Properties();
      shapeLayerProps.put("prettyName", "Political Solid");
      shapeLayerProps.put("lineColor", "000000");
      shapeLayerProps.put("fillColor", "BDDE83");
      shapeLayerProps.put("shapeFile", "resources/map/shape/dcwpo-browse.shp");
      shapeLayerProps.put("spatialIndex", "resources/map/shape/dcwpo-browse.ssx");

      ShapeLayer shapeLayer = new ShapeLayer();
      shapeLayer.setProperties(shapeLayerProps);

      // Add the political layer to the map
      mapBean.add(shapeLayer);
}

OpenMap应用程序中的图层可以使用来自许多来源的数据:

  • 通过计算
  • 来自本地硬盘驱动器的数据文件。
  • 来自URL的数据文件。
  • 来自jar文件中包含的数据文件。
  • 使用从数据库(JDBC)检索的信息。
  • 使用从地图服务器接收的信息(图像或地图对象)。
  1. 清单3显示了在initComponents()之后在MapFrame的构造函数中添加的新方法initMap() ,该方法显示了如何向ShapeLayer添加MapBean ,以呈现从shape (.shp)文件检索的政治边界图。 右键单击MapFrame类,然后选择Run File 您应该看到图2的窗口。做得好。

    OpenMap应用程序配置有openmap.properties file 该文件的内容指定创建哪些组件并将其添加到应用程序框架(包括层)中。 只需使用文本编辑器修改o penmap.properties文件,即可配置应用程序而无需重新编译。 只需将上述属性文件添加进去,就可以将已经了解框架的组件添加到应用程序中。 为使用属性编写的组件将获得其设置,以便正确初始化自己。 例如,依赖于数据文件或服务器位置的层通常具有允许在运行时设置这些位置的属性。 此属性文件通常位于应用程序文件夹中,或者位于用户的主文件夹中。 在后一种情况下,每个用户都可以根据自己的需求自定义应用程序。

    让我们将形状图层的属性移动到属性文件,然后从那里读取它们。

  1. 右键单击OpenMap项目,然后从弹出菜单中选择“ 新建”→“属性文件 ”。 给它命名属性,然后单击Finish
  2. 您可以在Projects窗口这个文件,显示文件窗口中单击和双击它在NetBeans编辑器中打开它。
  3. 粘贴清单4的内容。
  4. 注释掉在initMap()方法中设置形状层属性的行,并将其替换为清单5的代码。
  5. 再次运行该应用程序以查看完全相同的窗口(图2)。 清单4:openmap.properties
    prettyName=Political Solid
    lineColor=000000
    fillColor=BDDE83
    shapeFile=resources/map/shape/dcwpo-browse.shp
    spatialIndex=resources/map/shape/dcwpo-browse.ssx
    

    OpenMap提供了一个特殊的类来处理属性。 com.bbn.openmap.PropertyHandler是使用openmap.properties文件配置应用程序的组件。 可以知道在Java类路径和应用程序用户的主目录中,从哪个文件读取属性或将其留给自己来查找openmap.properties文件。

    清单5:initMap()的内容

          InputStream is = null;
          try {
             is = new FileInputStream("openmap.properties");
             shapeLayerProps.load(is);
          } catch (FileNotFoundException ex) {
             Logger.getLogger(OpenMap.class.getName()).log(Level.SEVERE, null, ex);
          } catch (IOException ex) {
             Logger.getLogger(OpenMap.class.getName()).log(Level.SEVERE, null, ex);
          } finally {
             if (is != null) {
                try {
                   is.close();
                } catch (IOException ex) {
                  Logger.getLogger(OpenMap.class.getName()).log(Level.SEVERE, null, ex);
                }
             }
          }
    
  1. 清单6显示了更新后的initMap()您不再需要Properties实例。 只需确保您告诉PropertyHandler.Builder()在本地目录(也称为./openmap.properties)中使用openmap.properties ,否则它可能会从用户的主目录或另一个位置中选取一个。 当然, PropertyHandler可以做的比这更多,我们将在以后的教程中看到。 清单6:使用PropertyHandler的initMap()内容
    private void initMap() {
       PropertyHandler propertyHandler = null;
       try {
          propertyHandler = new PropertyHandler.Builder().setPropertiesFile("./openmap.properties").build();
       } catch (IOException ex) {
           Logger.getLogger(MapFrame.class.getName()).log(Level.SEVERE, null, ex);
       }
       ShapeLayer shapeLayer = new ShapeLayer();
       if (propertyHandler != null) {
          shapeLayer.setProperties(propertyHandler.getProperties());
       }
    
       // Add the political layer to the map
       mapBean.add(shapeLayer);
    }
    

并发呢?

剩下的唯一障碍是我们将地图文件加载到EDT线程中。 如果我们需要加载一个大地图,这将延迟等待加载大地图的应用程序的启动。 我们需要将此任务委托给另一个线程。

有(至少)四种方法可以执行此操作:

  • javax.swing.SwingWorker
  • com.bbn.openmap.util.SwingWorker
  • java.awt.SecondaryLoop
  • java.util.concurrent.CompletableFuture

让我们开始看看它们中的每一个。

javax.swing.SwingWorker

传统方法是使用SwingWorker来完成脏工作(清单7)。 通用类SwingWorker提供了两种参数化类型。 第一个参数化类型( ShapeLayer )是doInBackground()get()方法的返回类型。 后台任务完成时, get()可访问doInBackground()返回的对象。 第二个参数化类型适用于定期发布的值。 当长时间运行的任务发布部分结果时,这很有用。 在这里,我们使用Void ,因为我们不发布部分结果。 doInBackground()的代码在后台线程中执行。 在这里,我们使用PropertyHandler读取属性,并创建并返回ShapeLayer

要启动后台线程,我们调用SwingWorker's execute()方法。 这样可以安排线程执行并立即返回。 后台任务完成后,将在EDT中调用overridden done()方法。 使用此方法可以在其中放置代码以更新或刷新GUI. Method get() GUI. Method get()阻塞,直到后台任务完成为止。 但是,如果在方法done()调用get() ,则由于后台任务已完成,因此不会发生阻塞。 在此方法中,我们将图层添加到mapBean 但是,由于已经渲染了MapFrame ,因此也需要刷新它才能渲染地图图层。 这是通过重新验证MapFrame来实现的。

清单7:使用javax.swing.SwingWorker的initMap()内容

private void initMap() {
   SwingWorker<ShapeLayer, Void> worker = new SwingWorker<ShapeLayer, Void>() {

     @Override
     public ShapeLayer doInBackground() {
       PropertyHandler propertyHandler = null;
       try {
           propertyHandler = new PropertyHandler.Builder().setPropertiesFile("./openmap.properties").build();
       } catch (IOException ex) {
          Logger.getLogger(MapFrame.class.getName()).log(Level.SEVERE, null, ex);
       }
       ShapeLayer shapeLayer = new ShapeLayer();
       if (propertyHandler != null) {
          shapeLayer.setProperties(propertyHandler.getProperties());
       }
       return shapeLayer;
     }

     @Override
     protected void done() {
        try {
            if (!isCancelled()) {
               // Add the political layer to the map
               mapBean.add(get());
               MapFrame.this.revalidate();
            }
        } catch (InterruptedException | ExecutionException ex) {
          Logger.getLogger(MapFrame.class.getName()).log(Level.SEVERE, null, ex);
        }
     }
   };
   // invoke background thread
   worker.execute();
}

com.bbn.openmap.util.SwingWorker

第二种解决方案使用OpenMap提供的SwingWorker (清单8)。 这是Java Swing的SwingWorker的简化版本。 参数化类型( ShapeLayer )是Construct()方法的返回类型。 在这里,我们将ShapeLayer的创建重构为其自己的方法getShapeLayer() (清单9)。

清单8:使用com.bbn.openmap.util.SwingWorker的initMap()内容

private void initMap() {
      com.bbn.openmap.util.SwingWorker<ShapeLayer> worker = new com.bbn.openmap.util.SwingWorker<ShapeLayer>() {

         @Override
         public ShapeLayer construct() {
            return getShapeLayer();
         }

         @Override
         public void finished() {
            // Add the political layer to the map
            mapBean.add(get());
            MapFrame.this.revalidate();
         }

      };
      // invoke background thread
      worker.execute();
}

要启动后台线程,我们调用SwingWorker的execute()方法。 这样可以安排线程执行并立即返回。 后台任务完成后,将在EDT中调用重写的finished()方法。 使用此方法可以在其中放置代码以更新或刷新GUI。 Method get()阻塞,直到后台任务完成为止。 但是,如果在方法finished()调用get() ,则由于后台任务已完成,因此不会发生阻塞。 在此方法中,我们将图层添加到mapBean 但是,由于已经渲染了MapFrame ,因此也需要刷新它才能渲染地图图层。 这是通过重新验证MapFrame来实现的。

如果添加Thread.sleep(10_000);则可以在快速计算机中完成此操作Thread.sleep(10_000); construct()方法中return语句之前的语句。 您应该看到应用程序的窗口没有等待SwingWorker完成其工作才能显示。

清单9:getShapeLayer()重构方法

private ShapeLayer getShapeLayer() {
    PropertyHandler propertyHandler = null;
    try {
        propertyHandler = new PropertyHandler.Builder().setPropertiesFile("./openmap.properties").build();
    } catch (IOException ex) {
        Logger.getLogger(MapFrame.class.getName()).log(Level.SEVERE, null, ex);
    }
    ShapeLayer shapeLayer = new ShapeLayer();
    if (propertyHandler != null) {
        shapeLayer.setProperties(propertyHandler.getProperties());
    }
//    try {
//        Thread.sleep(10_000);
//    } catch (InterruptedException ex) {
//        Logger.getLogger(MapFrame.class.getName()).log(Level.SEVERE, null, ex);
//    }
    return shapeLayer;
}

java.awt.SecondaryLoop

第三种解决方案使用SecondaryLoop(清单11)。 该接口提供了两种方法enter()exit() ,可用于启动和停止事件循环。 即使属性的加载和形状层的创建是在不同的线程中完成的,UI也没有响应,并且正在等待工作线程完成之后才在屏幕上呈现。

在JavaDoc中:“当调用enter()方法时, 当前线程将被阻塞,直到循环被exit()方法终止为止。 同样,新的事件循环在事件分发线程上启动,该线程可能是当前线程,也可能不是当前线程。 通过调用其exit()方法,可以在任何线程上终止该循环。 […]应用此接口的典型用例是AWT和Swing模态对话框。 当模式对话框显示在事件分发线程上时,它将进入一个新的辅助循环。 稍后,当对话框被隐藏或放置时,它退出循环,线程继续执行。” 换句话说,它确实阻塞了当前线程,因此在所有情况下都不是SwingWorker的“替代”。 没有像SwingWorker中那样的done()回调方法,您可以在不阻塞当前线程的情况下调用get()

清单10:使用SecondaryLoop的initMap()内容

private void initMap() {
   final ShapeLayer shapeLayer = new ShapeLayer();
   final SecondaryLoop loop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop();
   Thread work = new Thread() {

       @Override
       public void run() {
         PropertyHandler propertyHandler = null;
         try {
             propertyHandler = new PropertyHandler.Builder().setPropertiesFile("./openmap.properties").build();
         } catch (IOException ex) {
            Logger.getLogger(MapFrame.class.getName()).log(Level.SEVERE, null, ex);
         }

         if (propertyHandler != null) {
            shapeLayer.setProperties(propertyHandler.getProperties());
         }
         loop.exit();
      }

   };

   // We start the thread to do the real work  
   work.start();

   // Blocks until loop.exit() is called  
   loop.enter();

   // Add the political layer to the map
   mapBean.add(shapeLayer);
}

java.util.concurrent.CompletableFuture

Java 8提供了一个新类CompletableFuture CompletableFuture<T>通过提供功能性的单子运算并促进异步的,事件驱动的编程模型(而不是在较早的Java中进行阻止)来扩展Future<T>

您需要具有JDK 8或更高版本才能使用它。 如果没有,请右键单击OpenMap项目,然后选择Properties 选择类别库,然后选择Java 8 Java平台(您可能需要通过单击“ 管理平台”按钮并导航到下载和安装JDK 8的文件夹来添加新的Java 8平台)。 然后,选择Sources类别,并将Source / Binary Format更改为JDK 8

通常,Future表示由其他线程运行的一段代码,但它们不是异步的,即,您不能告诉它们异步执行任务并在将来的某个时间返回结果。 在这种情况下,您可以简单地创建一个CompletableFuture并将其返回给客户端,并且只要您认为结果可用,就可以complete()将来)并解锁所有等待该将来的客户端。 当然,像SwingWorker一样,有一个阻塞的get()方法。

CompletableFuture提供了异步方法和非异步方法,这些方法在与先前任务不同的另一个线程中执行其任务, 而非异步方法在与先前任务相同的线程中执行其任务。 异步方法中,任务被提交到fork-join线程池,完成后,结果将传递到下一个任务。 下一个任务完成时,其结果将进一步发送,依此类推。 它非常简洁明了。

清单11:使用CompletableFuture的initMap()内容

private void initMap() {
   CompletableFuture.supplyAsync(() -> getShapeLayer()).thenAcceptAsync(
      shapeLayer -> {
          // Add the political layer to the map
          mapBean.add(shapeLayer);
          MapFrame.this.revalidate();
      });
}

修改后的initMap()如清单11所示。您可以通过调用supplyAsync()并传递Supplier() -> getShapeLayer() )向JDK 8中引入的全局通用ForkJoinPool.commonPool()提供新任务。 。 如果您不想使用公共线程池,还有一个重写的supplyAsync()方法可以接受执行程序。 Supplier<R>是Java 8中引入的新接口,它不接受任何参数并返回类型R的值(在本例中为ShapeLayer )。

您可以使用thenApply()thenApplyAsync()方法(接受Function<T, R> )来应用进一步的处理,但是在我们的示例中thenApplyAsync()

您可以使用非阻塞的 thenAccept()thenAcceptAsync()方法异步地返回结果,该方法接受Consumer<T> 它们使您可以在准备就绪时消费未来的价值。 Consumer<T>Supplier<R>; 它接受类型T的参数并返回void

看看最后一个解决方案有多优雅。

结论

我们在OpenMap的第一个教程中走了很长一段路。 我们学习了如何在NetBeans IDE(它是一个Swing JFrame创建MapFrame ,并了解了如何使用IDE将OpenMap JavaBeans添加到Palette,然后将MapBean拖到MapFrame 我们学习了如何向MapBean添加图层以显示.shp地图文件。 通过属性文件配置层。 我们看到了如何使用PropertyHandler读取属性。 我们还看到了四种从不同线程加载地图文件的方法,即使地图文件加载时间过长,也可以使MapFrame保持响应。

在下一个教程中,我们将更深入地了解有关MapHandlerOpenMap内部。

翻译自: https://www.javacodegeeks.com/2015/10/openmap-tutorial-part-1.html

相关文章: