Cтатьи и комментарии

ЧИП № 9 2001

Разработка ГИС-приложений в среде Delphi/Kylix

С.Бабичев

Фукс этот - клад, а не матрос: прекрасно разбирается в картах!

Когда мы хотим узнать, где находится нужный нам дом, как проехать по некоторому адресу, мы ищем это по карте. К картам обращаются многие организации при составлении областей загрязнения окружающей среды, расположения природных ресурсов. С картами работают военные, МЧС, МВД, геологи, моряки, экологи, строители и многие другие специалисты. Трудно представить себе нашу жизнь без географических карт и без науки об их создании - картографии.

Конечно, эта весьма важная область деятельности, связанная со сложными математическими расчетами над большими объемами информации и отображением пространственных данных, не может развиваться без применения компьютеров. Поэтому в индустрии программного обеспечения был создан целый класс программных систем, называемый геоинформационными системами, или сокращенно ГИС.
ГИС служат для графического построения карт и получения информации как об отдельных объектах, так и пространственных данных об областях, например о расположении запасов природного газа, плотности транспортных коммуникаций или распределении дохода на душу населения в государстве. Отмеченные на карте области во многих случаях гораздо нагляднее отражают требуемую информацию, чем десятки страниц отчетов с таблицами.
В настоящее время наряду с разработками крупных западных производителей успешно применяются отечественные ГИС. Особенно следует отметить GeoGraph и GeoDraw, разработанные Институтом географии РАН ( http://geocnt.geonet.ru ) и ГИС "Карта 2000" (http://www.gisinfo.ru), разработанную Национальной картографической корпорацией (проект "Панорама").
Еще несколько лет назад большинство ГИС болели "замкнутостью", то есть для пользователя являлись некоей "вещью в себе". Система позволяла подготовить или открыть уже готовую карту, привязать ее к одной или нескольким таблицам в базе данных и с помощью встроенного оконного интерфейса ввести запрос на получение данных. По введенному запросу ГИС могла выдать в окне данные об объектах, показать их на карте, закрасить области различными видами штриховки, построить графики и отчеты. Фактически ГИС являлись средством создания карты и большим справочником по ней.
Однако потребности рынка диктовали свои условия разработчикам геоинформационных систем, и просто системы, отображающей на карте справочные данные с их элементарным анализом, стало явно недостаточно. Программистам, разрабатывающим сложные программные комплексы и системы поддержки принятия решений с использованием электронных карт, требовались инструменты для управления картой из своей программы. Требовался интерфейс программиста к геоинформационной системе (API), позволяющий из программы делать с картой то же, что делает с ней и сама ГИС.
Спрос рождает предложение, и такие средства разработчики геоинформационных систем стали предоставлять. Но подходы у разных фирм-производителей к этой проблеме были различными. Например, очень распространенная ГИС MapInfo предоставляет разработчикам язык MapBasic для написания собственных модулей, расширяющий ее функциональность подобно тому, как в текстовый процессор Microsoft Word входит WordBasic для написания макросов. Другие продукты решали эту проблему предоставлением программистам компонента OCX или ActiveX, умеющих работать с картами, созданными в их системе. Все это было не совсем удобно для работы. Наилучший способ - предоставление программистам, кроме ГИС, также и функциональной библиотеки работы с картами, созданными с ее помощью.
Такие средства стали появляться на рынке ПО в виде библиотек под конкретный язык программирования, чаще всего Microsoft Visual C++ или Visual Basic. Когда Borland создала мощную систему разработки приложений Delphi, которая стремительно завоевала популярность, разработчики ГИС стали также выпускать для нее свои библиотеки.
Задача выбора ГИС для реализации приложений очень непростая. Здесь приходится исходить не только из стоимости, возможностей и назначения, но также и из того, насколько хорошо ее библиотека согласуется с используемыми средствами программирования, достаточно ли предоставляет возможностей для обработки картографической информации, имеет ли средства подготовки данных, насколько быстро работает и какие требования предъявляет к ресурсам компьютеров. Однако обзор имеющихся на рынке геоинформационных систем - это отдельный и долгий разговор. По этим вопросам можно получить исчерпывающую информацию в журнале "ГИС - обозрение" или в ГИС-ассоциации ( http://www.gisa.gubkin.ru ).
Давайте рассмотрим одну из наиболее удачных отечественных разработок в области геоинформационных систем - ГИС "Карта 2000" (рис. 1).
Кроме развитых средств создания и редактирования электронных карт и библиотек условных знаков здесь имеется полнофункциональная библиотека программиста. Под Delphi и C++ Builder существует масса компонентов, набор функций API доступен не только из этих программных сред разработки, но и из Microsoft Visual C++, Borland C++, Watcom C++ и других.

Рис. 1. ГИС "Карта 2000"

Информация об объектах в системе "Карта 2000" хранится не только в значке, который его обозначает, но и в наборе параметров. Например, объект дорога может иметь семантики ширина, тип покрытия; объект река - семантики глубина, ширина, скорость течения, а также и другие характеристики. Объекты карты могут быть точечными, линейными, площадными и векторными. От значения некоторых полей семантики может зависеть вид этого объекта на карте. Например, если дорога была нанесена на карту как грунтовая, то после реконструкции с заменой ее типа покрытия в поле семантики достаточно занести код "асфальтобетон" и значок ее тут же изменится на соответствующий. Это, как мы дальше увидим, можно делать не только из "Карты 2000", но и из своего приложения. Вид информации по объекту показан на рис. 2.

Рис. 2. Семантика объекта

"Карта 2000" также позволяет по заданным высотам строить трехмерные модели выбранного участка местности. Высоты задаются для ряда точечных объектов в определенной семантике. Эта семантика указывается ГИС, и она автоматически строит матрицу высот, по которой показывает трехмерную модель местности (рис. 3).

Рис. 3. Построение трехмерной модели местности по матрице высот

Чтобы иметь возможность из своей программы, разрабатываемой на Delphi, работать с картами, требуется установить пакет GisToolKit. Сам пакет, как и ГИС, можно бесплатно получить на сайте http://panorama.lvl.ru , но создавать и использовать его в этом случае можно только для небольших карт, состоящих из одного листа масштаба 1:100 000. Чтобы использовать пакет в полной мере, нужно приобрести у производителей или дистрибьюторов и установить электронный ключ для LPT-порта.
Для инсталляции пакета GisToolKit в Delphi достаточно запустить программу Setup.exe из установочного пакета. Если версия Delphi не поддерживается программой установки, она не полностью завершается. В этом случае нужно завершить процесс инсталляции вручную. Для этого в среде Delphi откройте файл mappack.dpk из каталога, в который был установлен пакет, затем нажмите кнопку Install. В диалоге Tools - Environment Options - Library - Library Path добавьте в список путь к модулям пакета. Для установки справочной системы нужно выбрать в меню Help пункт Customize и добавить файл gis.hlp во всех закладках диалога настройки справочной системы Delphi.
Итак, установив набор компонентов GisToolKit, мы можем приступать к разработке своего ГИС-приложения. Приведенный ниже пример был реализован на Delphi 6; для других версий возможны незначительные изменения. Сначала ознакомимся с компонентами, которые появились в палитре Delphi на закладке GISToolKit (рис. 4).

Рис. 4. Компоненты GISToolKit

Окно, которое отображает карту, - это компонент MapView. Перенесем его с палитры на форму. Остальные компоненты, которые работают с ним, должны ссылаться на него через свойства MapView. То есть, когда мы помещаем компонент с палитры на форму, нужно "привязать" его к экземпляру MapView1, который находится на форме, указав в инспекторе объектов его имя.
Чтобы указать MapView1, какую карту нужно открыть, зададим его свойству MapFileName путь до файла карты. Это можно сделать прямо в Object Inspector, нажав на кнопку редактирования свойств. При этом откроется диалог выбора карты (рис. 5). Для примера выберем демонстрационную карту части Москвы и Московской области, прилагаемую к пакету. Она открывается, не требуя аппаратного ключа.

Рис. 5. Диалог выбора карты

Чтобы закрыть карту и освободить занимаемую ею память, свойству MapFileName нужно присвоить пустую строку. Это рекомендуется делать перед завершением работы программы.
Итак, карта выбрана, но в окне ее не видно. Чтобы показать карту, свойство MapView1.MapView нужно установить как True. Значение False гасит карту, но не закрывает ее, поэтому при последующей установке значения True она опять появится.
В процессе работы программы может потребоваться открывать различные карты. Для этого достаточно в коде присвоить свойству путь до другого файла карты, например "MapView1.MapFileName:= 'D:\MAPS\MYMAP.MAP';". Если мы хотим дать пользователю возможность самому выбирать карту, воспользуемся диалогом открытия файла.
Для выбора карты существует компонент OpenMapDialog. Вместо него можно воспользоваться и обычными средствами выбора файла, но этот компонент позволит выбрать карту в том же диалоге с просмотром и информацией о ней, который показан на рис. 5. Кроме того, код получится короче - всего одна строка:

procedure TForm1.OpenMapExecute(Sender: TObject);
begin
OpenMapDialog1.Execute; // загрузить карту
MapView1.MapView := True; // показать ее
end;

OpenDialog имеет свойство MapView, которое в инспекторе объектов должно быть присвоено тому компоненту MapView, которому он будет передавать выбранный файл карты, в данном случае MapView1.
Первое, что пользователь захочет, после того, как увидит карту, это изменить ее масштаб. MapView предоставляет два свойства - ViewScale для просмотра карты и PrintScale для вывода карты на печать. Оба свойства целые. В простейшем случае нажатием кнопки можно изменять ViewScale. Вболее сложном варианте можно определить переменную MouseAction, по нажатии кнопки активизировать код, обозначающий операцию смены масштаба в сторону увеличения или уменьшения, и затем по щелчку мыши на карте менять масштаб и центрировать карту в точке нажатия. Рассмотрим подробнее простой способ:

procedure TForm1.actZoomInExecute(Sender: TObject);
begin
MapView1.ViewScale := MapView1.ViewScale div 2;
end;

procedure TForm1.actZoomOutExecute(Sender: TObject);
begin
MapView1.ViewScale := MapView1.ViewScale * 2;
end;

При нажатии на одну из кнопок смены масштаба, связанных с действиями actZoomIn и actZoomOut, карта будет изменять масштаб в два раза.
Теперь разберемся с координатами и точками на карте. Точка на карте тоже имеет свой компонент MapPoint. Он предназначен для преобразования координат из одной системы в другую или для работы с точкой как с объектом. GISToolKit поддерживает четыре системы представления координат, обозначаемые константами:
PP_PLANE - прямоугольная система в метрах;
PP_GEO - геодезическая система в радианах;
PP_PICTURE - экранная система в пикселях;
PP_MAP- система карты в дискретах.
Для преобразования координат точки из одной системы в другую в классе TMapPoint и других классах пакета есть свойства PlaceIn (система координат на входе) и PlaceOut (система на выходе). Перенесем компонент MapPoint с палитры на форму и привяжем его к MapView1 установкой свойства MapPoint1.MapView в значение MapView1. Будем использовать этот компонент для перевода координат из экранной системы в метровую, и показывать их в статус-строке при обработке события OnMouseMove.
Установим его свойство PlaceIn в значение PP_PICTURE, а PlaceOut - в значение PP_PLANE. Теперь свойствам X и Y присвоим значения координат в одной системе, а считаем с них же уже преобразованные:

procedure TForm1.MapView1MouseMove(Sender: TObject; Shift: TShiftState; X,Y: Integer);
begin
MapPoint1.X := MapView1.MapLeft + X; // с учетом скроллинга
MapPoint1.Y := MapView1.MapTop + Y;
StatusBar1.Panels[0].Text := Format('X : %d Y : %d',[Trunc(MapPoint1.X),Trunc(MapPoint1.Y)]);
end;

Свойства MapView MapLeft и MapTop содержат координаты положения текущего окна карты от ее левого и верхнего края в пикселях. Поэтому, когда преобразовываются экранные координаты, обязательно нужно учитывать эти значения, чтобы точка позиционировалась правильно. Обратите внимание, что в геодезической системе координат ось X направлена снизу вверх, а ось Y справа налево.
Теперь перейдем к двум очень важным компонентам: MapObj - объект карты и MapFind - поиск объекта. О них нужно рассказать особо, так как именно эти компоненты и позволяют использовать в полной мере возможности ГИС.
Карта представляет собой "слоеный пирог" из множества наложенных друг на друга слоев, например слой рек и озер, слой лесов, слой дорожной сети, слой городских кварталов и т. п.
Каждый объект принадлежит одному определенному слою. Для указания, к какому слою принадлежит объект, MapObj имеет свойство ExCode. В нем содержится номер учетной записи этого слоя в классификаторе карты. Классификатор содержит записи обо всех слоях, включая то, какими значками они отображаются и какие семантики они могут содержать.
Свойство "код локализации" Local содержит информацию о типе объекта: точечный, линейный, площадной. Свойство Style задает вид отображения - нормальный, выделенный, невидимый. Свойства ColorImage и ColorImageUp определяют цвета, которыми объект будет "мигать" при выделении. Сам способ выделения задается свойствами StyleSelect и Interval.
Чтобы найти объект на карте по координатам, например по нажатию мышкой, и получить по нему информацию, содержащуюся в карте, напишем обработчик нажатия левой кнопки, используя метод SelectObjectInPoint:

procedure TForm1.MapView1MouseDown(Sender: TObject; Button: TMouseButton;Shift: TShiftState; X, Y: Integer);
begin
MapObj1.SelectObjectInPoint(X,Y,10,dlMaxSemantic); // объект ищется в радиусе 10 пикселей вокруг курсора
end;

Вот как будет выглядеть наше ГИС-приложение, если мы нажмем мышкой на путепровод в нижнем левом углу около Качалово (рис. 6).

Рис. 6. Пример работы простейшего ГИС-приложения

Одним из важных свойств объекта карты TMapObj является его уникальный номер на карте Key и имя листа карты ListName. Эти два свойства - ключ для поиска объекта местности на электронной карте.
Уникальный номер Key система назначает объекту автоматически при добавлении его в лист карты, и он служит его ключом для поиска в конкретной карте, так же как уникальный ключ записи в базах данных. В общем случае этого номера достаточно для организации взаимосвязи объекта карты с записью в базе данных, с которой работает приложение. Найдя объект в карте, по значению его свойства Key можно найти запись в базе данных, содержащую о нем нужную информацию, обычным SQL-запросом. И наоборот, если мы выбрали из базы данных запись о некоем объекте на карте, мы сможем его найти по этому значению и показать. Поиск объекта по его ключу является самым быстрым способом поиска, так как "Карта 2000" организована по принципу базы данных и все объекты в ней индексированы по их ключам. Если в приложении используется проект, состоящий из нескольких карт, или одна карта, включающая несколько стандартных планшетов (листов), в качестве уникальных данных для поиска объекта на карте необходимо обязательно использовать ГИС имя листа карты ListName вместе с номером объекта.
Допустим, некая фирма, владеющая сетью автозаправочных станций, имеет свою базу данных, в которой есть таблица fillingstations с адресами ее АЗС и количеством колонок на них:

id Name address num_pumps
1 АЗС № 14 ул. 1-я Парковая, 18 12
2 АЗС № 22 ул. 2-я Парковая, 8 10
3 АЗС № 23 ул. 3-я Парковая, 3 12
4 АЗС № 56 ул. 4-я Парковая, 22 16

Руководство фирмы решает улучшить свое программное обеспечение и предлагает программистам подключить электронную карту города. Предположим, готовая карта города куплена уже в формате ГИС "Карта 2000", и в ее слое "Заправочные станции", где есть все городские АЗС, те станции, которые принадлежат фирме, имеют ключи 16796116, 16796117, 16796126 и 16796148 соответственно (номера объектов взяты с демонстрационной карты). В таблицу fillingstations программисты добавили колонку key_map, заполнили ее, и теперь таблица выглядит следующим образом:

id name address num_pumps key_map
1 АЗС № 14 ул. 1-я Парковая, 18 12 16796116
2 АЗС № 22 ул. 2-я Парковая, 8 10 167961173 АЗС № 23 ул. 3-я Парковая, 3 12 167961264 АЗС № 56 ул. 4-я Парковая, 22 16 16796148

Дальнейшее очевидно. Если нам нужно найти на карте АЗС с максимальным количеством колонок, то мы выполняем SQL-запрос, в результате которого получаем, что ее id = 4, а ключ в карте 16796148.
Далее нужно найти такой объект на карте из приложения. Поставим на форму компонент MapFind и привяжем его к MapView1. Результат поиска MapFind помещает в объект MapObj. Поместим на форму еще один компонент MapObj2 и укажем его имя в свойстве MapFind1.MapObj. В нем будет содержаться информация о найденном в результате поиска объекте.
Для задания условий поиска MapFind имеет свойство MapSelect. Этот класс имеет множество настроек, которые задают, где и как искать нужные объекты - в каких слоях, каких типов, в какой области, с какими номерами или содержащие в семантике нужные значения. Диалог настройки вызывается при редактировании этого свойства (рис. 7).

Рис. 7. Настройка поисковой системы

Методы объектов позволяют настроить поиск непосредственно из приложения. Давайте найдем нужный нам объект по известному ключу. Поисковая система позволяет искать объекты в диапазоне ключей от минимального заданного до максимального заданного, но если нам нужно найти только один, значит, он будет и максимальным, и минимальным:

procedure TForm1.actSearchExecute(Sender: TObject);
begin
MapFind1.Active := False; // выключить поиск
MapFind1.MapSelect.MinKey := 16796148; // установить ключи поиска
MapFind1.MapSelect.MaxKey := 16796148;
MapFind1.Active := True; // включить поиск
MapFind1.Center; // передвинуть карту, чтобы найденный объект был в центре
end;

В приведенном примере кода будет найден один объект, поэтому нам не требуется организовывать цикл прохода по найденным объектам. В остальных случаях работа компонента MapFind аналогична работе компонента Query. Если найденных объектов много, то можно пройтись по ним таким же способом, как и по выбранным записям из базы данных:

MapFind1.Active := True;
while not MapFind1.Eof do begin
// Обработать объект MapFind1.MapObj;
MapFind1.Next;
end;
MapFind1.Active := False;

В заключение рассмотрим еще один пример, показывающий возможности GISToolKit. Нам может потребоваться нарисовать что-то свое на карте. Сделать это можно несколькими способами. Первый - добавить новый объект в карту. Второй - нарисовать поверх карты изображение средствами графической библиотеки Delphi. Третий - создать пользовательскую карту поверх основной и добавить в нее объекты из основного или собственного набора условных знаков.
Чтобы добавить новый объект, нужно воспользоваться методами класса TMapObj. По классификатору демонстрационной карты код слоя автозаправочных станций 51220000. Вот пример, который добавляет новую АЗС (рис. 8):

procedure TForm1.actInsertNewObjectExecute(Sender: TObject);
begin
MapPoint2.X := 6169253; // в точке с этими координатами открывается новая станция
MapPoint2.Y := 7411283;
MapObj2.CreateObjectByExcode(0,KM_IDFLOAT2,51220000,OL_MARK);
MapObj2.Metric.Append(0,MapPoint2.Point);
MapObj2.Commit;
// далее нужно записать в базу данных значение MapObj2.Key, если оно больше 0
end;

После операции Commit ГИС помещает новый объект в карту и генерирует ему уникальный ключ, который становится доступным через свойство Key. Если в результате операции Commit MapObj2.Key равен 0, то объект не создался и в карту добавлен не был. Новый объект на демонстрационной карте получил номер 16800828.

Рис. 8. Добавление новой автозаправочной станции

Нарисовать что-либо поверх карты можно на событии MapView.OnMapPaint. В параметр передается Canvas карты, на котором можно использовать методы и свойства обычного класса TCanvas. Для преобразования координат из метровых или градусных в экранные можно воспользоваться компонентом MapPoint, у которого свойство PlaceInp установлено в нужной системе, - в данном случае PP_PLANE, а PlaceOut в PP_PICTURE. Добавим на форму компонент MapPoint3 с такими установками и нарисуем вокруг новой АЗС круг радиусом 100 метров (рис. 9), за пределами которого от нее не будет пахнуть бензином:

procedure TForm1.MapView1MapPaint(Sender: TObject; Canvas: TCanvas);
var
X1, Y1, X2, Y2 : integer;
begin
MapFind1.Active := False;
MapFind1.MapSelect.MinKey : = 16800828; // найдем новую АЗС по ключу
MapFind1.MapSelect.MaxKey : = 16800828;
MapFind1.Active: = True;
// получим координаты левой верхней и правой нижней точки круга по 100 м от цента
MapPoint3.X : = MapFind1.MapObj.Metric.Points[0,1].X - 100;
MapPoint3.Y := MapFind1.MapObj.Metric.Points[0,1].Y - 100;
X1 := Trunc(MapPoint3.X);
Y1 := Trunc(MapPoint3.Y);
MapPoint3.X := MapFind1.MapObj.Metric.Points[0,1].X + 100;
MapPoint3.Y := MapFind1.MapObj.Metric.Points[0,1].Y + 100;
X2 := Trunc(MapPoint3.X);
Y2 := Trunc(MapPoint3.Y);
// теперь просто нарисуем круг с желтой штриховкой
Canvas.Brush.Color := clYellow;
Canvas.Brush.Style := bsBDiagonal;
Canvas.Ellipse(X1,Y1,X2,Y2);
MapFind1.Active := False; // выключим поиск
end;

Рис. 9. Область, нарисованная поверх карты

Обратите внимание, что радиус круга задан в метрах и преобразование метровых координат в экранные производится в момент рисования, а не заранее. Дело в том, что карта может быть изображена в любом масштабе и, естественно, размер пикселя в метрах на местности при разных масштабах будет разным. Только в обработчике события перерисовки карты MapView.OnMapPaint мы точно можем знать текущий масштаб вывода и, соответственно, размер одного пикселя в метрах.
В этом случае радиус нарисованного круга при любом масштабе будет соответствовать 100 метрам. Рассмотрение третьего способа - создание пользовательской карты - весьма увесистая тема, и сегодня мы ее рассматривать не будем, впрочем, об этом можно прочитать в документации на сайте разработчиков.
В последнее время часто возникает задача переноса разработанных пользовательских приложений с ОС Windows под OC Linux. ГИС-приложения, созданные с использованием GISToolKit, могут быть успешно и без больших затрат перекомпилированы под OC Linux. Для этого необходимо наличие Kylix (аналог Delphi для Linux) и соответствующей версии инструментария GISToolKit.
Мы рассмотрели лишь малую часть возможностей пакета GISToolKit. Набор компонент и набор функций MapAPI, поставляемый разработчиками в пакете, позволяют создавать очень сложные профессиональные ГИС-приложения. Тем не менее нам удалось построить небольшое ГИС-приложение, написав вручную всего лишь три десятка строк кода. Согласитесь, это очень неплохо.