介绍
本系列教程将向您展示如何使用OpenMap GIS Java Swing库构建Java应用程序。
OpenMap的开发人员指南是非常有用的文档,描述了OpenMap的体系结构,但没有说明如何逐步启动和构建应用程序。 源代码附带的示例很有用,但还不够。
OpenMap是用Swing编写的。 在撰写本文时,最新版本是5.1.12。 您可以从GitHub下载源代码和可执行jar。 将其复制/解压缩/克隆到目录后,可以通过运行适用于您平台的相关脚本( openmap.bat
或openmap
)或双击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应用程序:
- 打开菜单File→New Project,然后选择Category : Java和Project : Java Application (图3)。 单击下一步 。
- 在下一步中,提供名称和位置。 确保对库使用专用文件夹,并且不要选择主类(图4)。 点击完成 。
- 创建新项目后,通过右键单击Source Packages并从弹出菜单中选择New→Java Package ,创建一个名为
openmap
的新软件包。 - 右键单击“ 库”文件夹,然后从弹出菜单中选择“ 添加JAR /文件夹 ”操作。 导航到OpenMap安装的
lib
文件夹,然后选择openmap.jar
。 您可以使用相对路径,也可以将其更好地复制到Libraries文件夹中(图5)。 单击“ 打开”关闭对话框!
- 您还需要复制地图文件。 最常见的格式是
.shp
(ESRI Shape)。 通过在NetBeans中选择“ 文件”窗口,右键单击OpenMap1
项目,然后从弹出菜单中选择“ 新建”→“文件夹” ,创建一个新的文件夹层次结构resources/map
。 输入名称resources
,然后单击确定 。 右键单击resources
文件夹,然后重复该过程以在其中创建地图文件夹。 将share/data/shape
文件夹从您的OpenMap安装复制到map
文件夹 - 右键单击
openmap
包,然后从弹出菜单中选择New→JFrame Form ,以创建新的JFrame
表单。 给它起一个名字,例如MapFrame ,然后单击Finish 。
- 单击Source按钮以查看生成的代码(请参见清单1)。
- 添加行
super("Simple Map");
在构造函数中设置窗口标题。 - 构造函数初始化
JFrame
。 到目前为止,没有添加任何内容。 由于它是一个GUI应用程序,因此需要在EDT线程中运行,这就是NetBeans在main()
方法中为我们编写的内容。 - 单击返回到“ 设计”按钮以查看空白表格。
我们可以将OpenMap JavaBeans添加到面板中。 要做到这一点:
- 右键单击调色板,然后选择“ 调色板管理器” 。
- 单击“ 新建类别”,然后输入OpenMap作为类别名称。 单击单击从JAR添加按钮,导航到
openmap.jar
,选择显示所有JavaBeans单选按钮并选择所有可用组件。 单击下一步。 - 选择OpenMap调色板类别,然后单击Finish 。 新的调色板类别已添加到“调色板”中。
添加地图
- 找到
MapBean
并将其拖动到MapFrame
。 - 在NetBeans的Navigator窗口中,右键单击
mapBean1
,选择Change Variable Name并将其设置为mapBean
。 - 在“ 导航器”窗口中,右键单击
JFrame
并将其布局更改为BorderLayout
- 结果代码如清单2所示。
com.bbn.openmap.MapBean
组件是OpenMap工具包中的主要地图窗口组件。 MapBean
派生自java.awt.Container
类。 因为它是Swing组件,所以可以像其他任何用户界面组件一样将其添加到Java窗口层次结构中。
为了在MapBean
创建地图,需要将Layers (com.bbn.openmap.Layer)
添加到MapBean
。 图层派生自java.awt.Component
,它们是可以添加到MapBean的唯一组件。 因为Layers
是MapBean
容器中包含的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() {@Overridepublic 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 mapmapBean.add(shapeLayer);
}
OpenMap应用程序中的图层可以使用来自许多来源的数据:
- 通过计算
- 来自本地硬盘驱动器的数据文件。
- 来自URL的数据文件。
- 来自jar文件中包含的数据文件。
- 使用从数据库(JDBC)检索的信息。
- 使用从地图服务器接收的信息(图像或地图对象)。
- 清单3显示了在
initComponents()
之后在MapFrame
的构造函数中添加的新方法initMap()
,该方法显示了如何向ShapeLayer
添加MapBean
,以呈现从shape (.shp)
文件检索的政治边界图。 右键单击MapFrame
类,然后选择Run File 。 您应该看到图2的窗口。做得好。OpenMap应用程序配置有
openmap.properties file
。 该文件的内容指定创建哪些组件并将其添加到应用程序框架(包括层)中。 只需使用文本编辑器修改openmap.properties
文件,即可配置应用程序而无需重新编译。 只需将上述属性文件添加进去,就可以将已经了解框架的组件添加到应用程序中。 为使用属性编写的组件将获得其设置,以便正确初始化自己。 例如,依赖于数据文件或服务器位置的层通常具有允许在运行时设置这些位置的属性。 此属性文件通常位于应用程序文件夹中,或者位于用户的主文件夹中。 在后一种情况下,每个用户都可以根据自己的需求自定义应用程序。让我们将形状图层的属性移动到属性文件,然后从那里读取它们。
- 右键单击OpenMap项目,然后从弹出菜单中选择“ 新建”→“属性文件 ”。 给它命名属性,然后单击Finish 。
- 您可以在Projects窗口这个文件,显示文件窗口中单击和双击它在NetBeans编辑器中打开它。
- 粘贴清单4的内容。
- 注释掉在
initMap()
方法中设置形状层属性的行,并将其替换为清单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);}}}
- 清单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 mapmapBean.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>() {@Overridepublic 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;}@Overrideprotected void done() {try {if (!isCancelled()) {// Add the political layer to the mapmapBean.add(get());MapFrame.this.revalidate();}} catch (InterruptedException | ExecutionException ex) {Logger.getLogger(MapFrame.class.getName()).log(Level.SEVERE, null, ex);}}};// invoke background threadworker.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>() {@Overridepublic ShapeLayer construct() {return getShapeLayer();}@Overridepublic void finished() {// Add the political layer to the mapmapBean.add(get());MapFrame.this.revalidate();}};// invoke background threadworker.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() {@Overridepublic 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 mapmapBean.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 mapmapBean.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
保持响应。
在下一个教程中,我们将更深入地了解有关MapHandler
的OpenMap内部。
翻译自: https://www.javacodegeeks.com/2015/10/openmap-tutorial-part-1.html