Вступление
Эта серия учебных пособий покажет вам, как создавать приложения 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 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
для настройки приложения. Можноopenmap.properties
изopenmap.properties
файла считывать свойства, или оставить его самостоятельно, чтобы найти файлopenmap.properties
в пути к классам Java и в домашнем каталоге пользователя приложения.Листинг 5: содержимое initMap ()
0102030405060708091011121314151617InputStream 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
. Просто убедитесь, что выopenmap.properties
PropertyHandler.Builder()
использоватьopenmap.properties
в локальном каталоге (он же ./openmap.properties), в противном случае он может забрать один из домашнего каталога пользователя или другого местоположения. Конечно,PropertyHandler
может сделать намного больше, чем это, как мы увидим в будущих уроках. Листинг 6: содержимое initMap () с использованием PropertyHandler010203040506070809101112131415private
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
класс 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
.