Вступление
Эта серия учебных пособий покажет вам, как создавать приложения Java с использованием библиотеки OpenMap GIS Java Swing.
Руководство разработчика OpenMap — очень полезный документ, описывающий архитектуру OpenMap, но в нем не объясняется, как пошагово запускать и создавать приложение. Примеры, которые идут вместе с исходным кодом, полезны, но их недостаточно.
OpenMap написан на Swing. На момент написания этой статьи последняя версия 5.1.12. Вы можете скачать как исходный код, так и исполняемые файлы из GitHub . После того, как вы скопируете / распакуете / клонируете его в каталог, вы можете выполнить его, запустив соответствующий скрипт для вашей платформы ( openmap.bat или openmap ) или дважды щелкнув по lib/openmap.jar . Вы должны увидеть полное ГИС-приложение, подобное изображенному на рисунке 1. Мы попытаемся создать подобное приложение к концу этой серии. Исходный код OpenMap также содержит некоторые примеры использования OpenMap. В этом уроке мы будем опираться на com.bbn.openmap.app.example.SimpleMap . Во втором уроке мы будем использовать код из com.bbn.openmap.app.example.SimpleMap2 . Последующие уроки будут основаны на других примерах.
В этой серии руководств мы будем использовать новейшую среду IDE NetBeans 8.1 для создания наших приложений.
Урок 1. Создание базового приложения для карт.
Создать приложение JFrame
В этом первом уроке мы JFrame базовое приложение JFrame , содержащее карту (см. Рисунок 2). Откройте NetBeans и создайте новое приложение Java, выполнив следующие действия:
- Откройте меню File → New Project и выберите Category : Java и Project : Java Application (рисунок 3). Нажмите Далее .
- На следующем шаге укажите имя и местоположение. Убедитесь, что вы используете выделенную папку для библиотек и не выбираете основной класс (рисунок 4). Нажмите на Готово .
- Как только ваш новый проект будет создан, создайте новый пакет с именем
openmap, щелкнув правой кнопкой мыши на Source Packages и выбрав New → Java Package из всплывающего меню. - Щелкните правой кнопкой мыши папку « Библиотеки » и выберите действие « Добавить JAR / папку» во всплывающем меню. Перейдите в папку
libвашей установки OpenMap и выберитеopenmap.jar. Вы можете использовать относительный путь или лучше скопировать его в папку « Библиотеки » (рисунок 5). Нажмите Открыть, чтобы закрыть диалоговое окно! - Вам также необходимо скопировать файлы карты. Наиболее распространенным форматом является
.shp(ESRI Shape). Создайте новую папкуresources/map, выбрав окно « Файлы» в NetBeans, щелкнув правой кнопкойOpenMap1проектOpenMap1и выбрав « Создать» → «Папка» во всплывающем меню. Введите имяresourcesи нажмите ОК . Щелкните правой кнопкой мыши папкуresourcesи повторите процедуру, чтобы создать в ней папку карты. Скопируйте папкуshare/data/shapeиз вашей установки OpenMap в папкуmap - Создайте новую форму
openmapщелкнув правой кнопкойopenmapпакетopenmapи выбрав New → JFrame Form из всплывающего меню. Дайте ему имя, например, MapFrame и нажмите « Готово» . - Нажмите кнопку Source , чтобы увидеть сгенерированный код (см. Листинг 1).
- Добавьте строку
super("Simple Map");в конструкторе, чтобы установить заголовок окна. - Конструктор инициализирует
JFrame. Пока ничего не добавлено. Поскольку это приложение с графическим интерфейсом, оно должно выполняться в потоке EDT, и это то, что NetBeans написал для нас в методеmain(). - Нажмите на кнопку « Дизайн» , чтобы увидеть пустую форму.
Мы можем добавить OpenMap JavaBeans в палитру. Для этого:
- Щелкните правой кнопкой мыши палитру и выберите « Диспетчер палитр».
- Нажмите на новую категорию и введите OpenMap в качестве имени категории. Нажмите кнопку « Добавить из JAR» , перейдите к
openmap.jar,openmap.jarпереключатель « Показать все JavaBeans » и выберите все доступные компоненты. Нажмите Далее. - Выберите категорию палитры OpenMap и нажмите « Готово» . Новая категория палитры была добавлена в палитру.
Добавить карту
- Найдите
MapBeanи перетащите его вMapFrame. - В окне навигатора NetBeans щелкните правой кнопкой мыши
mapBean1, выберите « Изменить имя переменной» и установите для него значение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 являются Components содержащимися в контейнере MapBean , рендеринг Layers на карту контролируется механизмом рендеринга компонентов Java. Этот механизм контролирует, как слоистые компоненты окрашены друг на друга. Чтобы убедиться, что каждый компонент отображается в окне в правильном порядке, класс Component включает метод, который позволяет ему сообщать механизму рендеринга, что он хотел бы быть нарисованным. Эта функция позволяет Layers работать независимо друг от друга и позволяет MapBean не знать, что происходит на слоях.
Листинг 1: Базовое приложение Swing
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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
|
1
2
3
4
5
6
7
8
9
|
@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
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
/** * 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).
- Использование информации, полученной от картографического сервера (изображения или объекты карты).
- В листинге 3 показан новый метод
initMap()добавленный в конструкторMapFrameпослеinitComponents()который показывает, как добавитьShapeLayerвMapBean, чтобы визуализировать карту политических границ, извлеченную из файловshape (.shp). Щелкните правой кнопкойMapFrameклассMapFrameи выберите « Запустить файл» . Вы должны увидеть окно рисунка 2. Хорошо сделано.Приложение OpenMap настроено с помощью
openmap.properties file. Содержимое этого файла указывает, какие компоненты создаются и добавляются в платформу приложения, включая слои. Приложения можно настроить без перекомпиляции, просто изменив файл openmap.propertiesс помощью текстового редактора. Компоненты, которые были написаны с пониманием структуры, могут быть добавлены в приложение, просто сделав дополнения в вышеупомянутый файл свойств. Компоненты, написанные для использования свойств, получат свои настройки для правильной инициализации. Например, слои, которые зависят от расположения файлов данных или серверов, обычно имеют свойства, позволяющие устанавливать эти расположения во время выполнения. Этот файл свойств обычно находится либо в папке приложения, либо лучше в домашней папке пользователя. В последнем случае каждый пользователь может настроить приложение под свои нужды.Давайте переместим свойства слоя формы в файл свойств и прочитаем их оттуда.
- Щелкните правой кнопкой мыши проект OpenMap и выберите « Создать» → «Свойства файла» во всплывающем меню. Дайте ему имя свойства и нажмите « Готово» .
- Вы можете просмотреть этот файл в окне » Проекты» , показать щелчок в окне » Файлы» и дважды щелкнуть по нему, чтобы открыть его в редакторе NetBeans.
- Вставьте содержимое листинга 4.
- Закомментируйте строки, в которых свойства слоя формы установлены в
initMap()и замените их кодом из листинга 5. - Запустите приложение еще раз, чтобы увидеть то же самое окно (рисунок 2). Листинг 4: openmap.properties
12345
prettyName=Political SolidlineColor=000000fillColor=BDDE83shapeFile=resources/map/shape/dcwpo-browse.shpspatialIndex=resources/map/shape/dcwpo-browse.ssxOpenMap предоставляет специальный класс для обработки свойств.
com.bbn.openmap.PropertyHandler— это компонент, который использует файлopenmap.propertiesдля настройки приложения. Можноopenmap.propertiesизopenmap.propertiesфайла считывать свойства, или оставить его самостоятельно, чтобы найти файлopenmap.propertiesв пути к классам Java и в домашнем каталоге пользователя приложения.Листинг 5: содержимое initMap ()
0102030405060708091011121314151617InputStream is =null;try{is =newFileInputStream("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. Просто убедитесь, что выopenmap.propertiesPropertyHandler.Builder()использоватьopenmap.propertiesв локальном каталоге (он же ./openmap.properties), в противном случае он может забрать один из домашнего каталога пользователя или другого местоположения. Конечно,PropertyHandlerможет сделать намного больше, чем это, как мы увидим в будущих уроках. Листинг 6: содержимое initMap () с использованием PropertyHandler010203040506070809101112131415privatevoidinitMap() {PropertyHandler propertyHandler =null;try{propertyHandler =newPropertyHandler.Builder().setPropertiesFile("./openmap.properties").build();}catch(IOException ex) {Logger.getLogger(MapFrame.class.getName()).log(Level.SEVERE,null, ex);}ShapeLayer shapeLayer =newShapeLayer();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 класс SwingWorker предоставляет два параметризованных типа. Первый параметризованный тип ( ShapeLayer ) является типом возврата для методов doInBackground() и get() . Объект, возвращаемый doInBackground() становится доступным doInBackground() get() после завершения фоновой задачи. Второй параметризованный тип применяется к периодически публикуемым значениям. Это полезно, когда долгосрочные задачи публикуют частичные результаты. Здесь мы используем Void , так как мы не публикуем частичные результаты. Код внутри doInBackground() выполняется в фоновом потоке. Здесь мы читаем свойства с помощью PropertyHandler и создаем и возвращаем ShapeLayer .
Чтобы запустить фоновый поток, мы вызываем SwingWorker's execute() . Это планирует поток для выполнения и немедленно возвращает. overridden done() метод overridden done() вызывается в EDT после завершения фоновой задачи. Этот метод — то, куда вы помещаете код, чтобы обновить или обновить GUI. Method get() GUI. Method get() блокирует, пока фоновая задача не завершится. Однако если вы вызываете get() внутри метода done() , блок не возникает, поскольку фоновая задача завершена. В этом методе мы добавляем слой в mapBean . Однако, поскольку MapFrame уже визуализирован, его также необходимо MapFrame для отображения слоев карты. Это достигается путем повторной MapFrame .
Листинг 7: содержимое initMap () с использованием javax.swing.SwingWorker
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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
Второе решение использует SwingWorker предоставляемый OpenMap (листинг 8). Это упрощенная версия SwingWorker Java Swing. Параметризованный тип ( ShapeLayer ) является типом возврата метода construct (). Здесь мы реорганизовали создание ShapeLayer для его собственного метода getShapeLayer() (листинг 9).
Листинг 8: содержимое initMap () с использованием com.bbn.openmap.util.SwingWorker
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
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();} |
Чтобы запустить фоновый поток, мы вызываем метод execute() SwingWorker. Это планирует поток для выполнения и немедленно возвращает. Переопределенный метод finish finished() вызывается в EDT после завершения фоновой задачи. Этот метод — то, куда вы помещаете код, чтобы обновить или обновить GUI. Method get() блокирует, пока фоновая задача не завершится. Однако, если вы вызываете get() в методе finished() , блок не возникает, поскольку фоновая задача завершена. В этом методе мы добавляем слой в mapBean . Однако, поскольку MapFrame уже визуализирован, его также необходимо MapFrame для отображения слоев карты. Это достигается путем повторной MapFrame .
Вы можете сделать это более очевидно на быстрой машине, если добавите Thread.sleep(10_000); оператор перед оператором return в методе construct() . Вы должны увидеть, что окно приложения не ждет, пока SwingWorker завершит свою работу для отображения.
Листинг 9: метод рефакторинга getShapeLayer ()
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
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() , которые можно использовать для запуска и остановки цикла событий. Несмотря на то, что загрузка свойств и создание слоя формы выполняются в другом потоке, пользовательский интерфейс не реагирует и ожидает завершения рабочего потока, прежде чем он будет отображен на экране.
Из JavaDoc: «Когда вызывается метод enter() , текущий поток блокируется, пока цикл не завершится методом exit() . Кроме того, новый поток событий запускается в потоке диспетчеризации событий, который может быть или не быть текущим потоком. Цикл можно завершить в любом потоке, вызвав его метод exit() . […] Типичный вариант применения этого интерфейса — модальные диалоги AWT и Swing. Когда модальный диалог отображается в потоке диспетчеризации событий, он входит в новый вторичный цикл. Позже, когда диалог скрыт или расположен, он выходит из цикла, и поток продолжает свое выполнение ». Другими словами, он блокирует текущий поток, поэтому он не является «заменой» SwingWorker для всех случаев. Не существует метода обратного вызова done() как в SwingWorker где вы можете вызвать get() не блокируя текущий поток.
Листинг 10: содержимое initMap () с использованием SecondaryLoop
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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> расширяет Future<T> , предоставляя функциональные монадические операции и продвигая асинхронную модель программирования, управляемую событиями, в отличие от блокировки в более старой Java.
Вы должны иметь JDK 8 или более поздней версии, чтобы иметь возможность использовать его. Если вы этого не сделаете, щелкните правой кнопкой мыши проект OpenMap и выберите Свойства . Выберите категорию « Библиотеки» и выберите платформу Java 8 Java (вам может потребоваться добавить новую платформу Java 8, нажав кнопку « Управление платформами» и перейдя в папку, в которую вы загрузили и установили JDK 8). Затем выберите категорию « Источники » и измените « Исходный / двоичный формат» на JDK 8 .
Обычно Futures представляют фрагмент кода, выполняемый другим потоком, но они не являются асинхронными, то есть вы не можете сказать им, чтобы они выполняли задачу асинхронно и возвращались когда-нибудь в будущем с результатом. В этом случае вы можете просто создать CompletableFuture, вернуть его своему клиенту, и, когда вы считаете, что ваши результаты доступны, просто complete() будущее и разблокируйте всех клиентов, ожидающих этого будущего. Конечно, есть блокирующий метод get() как в случае с SwingWorker.
CompletableFuture предоставляет асинхронные методы, которые выполняют свою задачу в другом потоке, чем предыдущая, а также не асинхронные методы, которые выполняют свою задачу в том же потоке, что и предыдущая задача. В асинхронных методах задача передается в пул потоков fork-join, а когда она заканчивается, результат передается следующей задаче. Когда следующая задача заканчивается, ее результат отправляется дальше и так далее. Это довольно аккуратно и просто.
Листинг 11: содержимое initMap () с использованием CompletableFuture
|
1
2
3
4
5
6
7
8
|
private void initMap() { CompletableFuture.supplyAsync(() -> getShapeLayer()).thenAcceptAsync( shapeLayer -> { // Add the political layer to the map mapBean.add(shapeLayer); MapFrame.this.revalidate(); });} |
Модифицированный initMap () показан в листинге 11. Вы можете предоставить новую задачу глобальному ForkJoinPool.commonPool() общего назначения, представленному в JDK 8, вызывая supplyAsync() и передавая Supplier ( () -> getShapeLayer() ) , Существует также переопределенный supplyAsync() который принимает Executor, если вы не хотите использовать общий пул потоков. Supplier<R> — это новый интерфейс, представленный в Java 8, который не принимает параметров и возвращает значение типа R (в нашем случае это ShapeLayer ).
Вы можете применить дальнейшую обработку, используя thenApply() или thenApplyAsync() (которые принимают Function<T, R> ), но в нашем примере это не требуется.
Вы можете получить результат обратно асинхронно, используя неблокирующие thenAccept() или thenAcceptAsync() , которые принимают Consumer<T> . Они позволяют вам потреблять будущую стоимость, когда она будет готова. Consumer<T> является противоположностью Supplier<R>; он принимает параметр типа T и возвращает void .
Посмотрите, как элегантно это последнее решение.
Вывод
Мы прошли долгий путь в этом первом уроке OpenMap. Мы узнали, как создать MapFrame в IDE NetBeans, которая представляет собой Swing JFrame и увидели, как использовать IDE для добавления OpenMap JavaBeans в палитру, а затем перетащить MapBean на MapFrame . Мы узнали, как добавлять слои в MapBean для отображения файлов карты MapBean . Слои настраиваются через файлы свойств. Мы увидели, как использовать PropertyHandler для чтения наших свойств. Мы также увидели четыре способа загрузки файлов нашей карты из другого потока, чтобы обеспечить MapFrame нашего MapFrame даже если файлы карты загружаются слишком долго.
В следующем уроке мы углубимся во внутренности OpenMap и узнаем о MapHandler .




