уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

В предыдущей части мы научились ставить метки на карте Google Earth, используя возможности kml в delphi. В числе прочего мы также научились делать подписи к меткам, добавлять описание к документу и меткам внутри этого kml-документа. Сегодня будем учиться рисовать на карте линии и полигоны.

Прежде, чем начнем рисовать линии и полигоны на карте в Google Earth небольшое уточнение по предыдущей части цикла статей про KML Reference.

В первой статье, под названием «KML Reference в Delphi по-русски #1: Базовые классы для работы с метками«, я создал вот такой класс для работы с метками:

TKmlPlacemark = class(TKmlFeature)
  private
    FPoint: TKmlPoint;
  public
    constructor Create;
    destructor Destroy;override;
    property Point: TKmlPoint read FPoint;
  end;

в предположении о том, что элемент Placemark может определять только метки (точки) на карте. Однако, это не так. Дело в том, что, согласно KML Reference, Placemark может содержать любые элементы Geometry (не только точки). Эта ошибка была обнаружена как раз, когда начал разбираться с рисованием линий. Поэтому сегодня мы немного изменим этот класс и адаптируем его под использование в самых различных случаях, включая и использование класса TKmlPlacemark для рисования сколь угодно сложных фигур на карте Google Earth.

Виды линий и полигоны в KML

KML предлагает на выбор следующие геометрические элементы:

  1. LineString — незамкнутая линия (или, по другому, путь)
  2. LinearRing — замкнутая линия
  3. Polygon — полигон.

По сути, LineString и LinearRing — это обычные линии различие между которыми состоит в том, что для рисования LineString необходимо задать минимум две точки (начало и конец линии), а для LinearRing — четыре. Почему четыре? Дело в том, что замыкающая (последняя) точка у LinearRing должна быть точно такой же, как и первая. То есть, порядок отрисовки линий в KML выглядит следующим образом:

Порядок отрисовки LineString

Порядок отрисовки LinearRing

Также, у LineString можно задать порядок отрисовки линии, если элемент Placemark будет содержать несколько элементов LineString, о этой возможностью мы пользоваться пока не будем, так как пока нам предстоит только научиться рисовать всякие примитивы.

Что касается элемента Polygon (полигон), то этот геометрический объект может иметь как внешнюю, так и внутренние границы. При этом, с помощью набора внутренних границ мы можем «вырезать» в полигоне участки, например, нарисовать вот такую фигуру:

Полигон у которого заданы две внутренние границы

Теперь приступим к доработке нашего модуля kml.pas.

KML в Delphi

Для начала немного доработаем класс TKmlGeometry, который у нас представляем собой любой геометрический объект на карте. Как мы разбирались ранее, элемент Geometry не может напрямую создаваться в KML-документе, однако, этот элемент обладает общими для всех других геометрических объектов свойствами, а именно:

  • Extrude: boolean — «выдавливание» элемента на карте
  • AltitudeMode: TAltitudeModeEnum — режим «выдавливания»: прижать к земле, поднять над уровнем моря и так далее.

Об этих свойствах я подробно рассказывал в первой статье, посвященной KML в Delphi.

Исходя из этого, класс TKmlGeometry можно представить следующим образом:

TKmlGeometry = class(TKmlObject)
  private
    FExtrude: boolean;
    FAltitudeMode: TAltitudeModeEnum;
  public
    constructor Create; virtual;
    destructor Destroy; override;
    property Extrude: boolean read FExtrude write FExtrude;
    property AltitudeMode: TAltitudeModeEnum read FAltitudeMode write FAltitudeMode;
  end;

Теперь перед нами стоит задача — сделать так, чтобы наш класс TKmlPlacemark мог содержать в себе любой геометрический элемент, а именно: Point, LineString, LinearRing, Polygon или любого другого наследника от Geometry (см. диаграмму элементов kml).

Для решения этой задачки используем метаклассы в Delphi или, по-другому class reference. Надо сказать, что эту возможность в Delphi я использовал не часто, поэтому могу в чем-то ошибиться, но, представленный ниже код будет работать :)

Итак, делаем такое объявление:

TKmlGeometryClass = class of TKmlGeometry;

и переписываем TKmlPlacemark следующим образом:

TKmlPlacemark = class(TKmlFeature)
  private
    FGeometry: TKmlGeometry;
  public
    constructor Create(AGeometryClass: TKmlGeometryClass); overload;
    destructor Destroy; override;
    function Save(AParentNode: IXMLNode): IXMLNode; override;
    property Geometry: TKmlGeometry read FGeometry;
  end;
 
constructor TKmlPlacemark.Create(AGeometryClass: TKmlGeometryClass);
begin
  inherited Create;
  FGeometry := AGeometryClass.Create
end;

Таким образом, мы можем делать такие вызовы конструктора TKmlPlacemark:

Placemark:=TKmlPlacemark.Create(TKmlPolygon); //будем рисовать полигон
Placemark:=TKmlPlacemark.Create(TKmlPoint); //будем рисовать метку
Placemark:=TKmlPlacemark.Create(TKmlLineString); //будем рисовать линию
//и так далее

Далее, в зависимости от того, какой геометрический элемент будет представлять Placemark, мы сможем использовать свойства этих объектов. Следующий класс, который нам желательно создать — это список точек. Конечно, можно было бы ограничиться массивом, но со списком, на мой взгляд, в итоге, код получится немного короче и понятнее. Итак, для хранения набора точек будем использовать следующий тип данных:

TKmlCoordinateList = class(TObjectList)
  public
    function ToString: string; override;
    function AddCoordinate(ALongtitude, ALatitude, AAltitude: double): integer;
  end;
Знаю, что плагин для подсветки кода Delphi может хаотично «жрать» треугольные скобки в коде или вообще делать из них форматированные html-теги, поэтому пишу словами TKmlCoordinateList это список-дженерик (TObjectList), который содержит объекты TKmlCoordinate. Надеюсь, что когда-нибудь этот баг в плагине будет исправлен :)
Функция ToString предназначена для формирования строки, содержащей, так называемые, кортежи точек в соответствии с KML Reference:
function TKmlCoordinateList.ToString: string;
var
  I: integer;
  SB: TStringBuilder;
begin
  SB := TStringBuilder.Create;
  try
    for I := 0 to Pred(Count) do
    begin
      SB.Append(Items[I].ToString);
      if I < Pred(Count) then
        SB.Append(' ');
    end;
    Result := SB.ToString(True);
  finally
    FreeAndNil(SB);
  end;
end;

Функция AddCoordinate позволяет добавлять в список очередную точку:

function TKmlCoordinateList.AddCoordinate(ALongtitude, ALatitude,
  AAltitude: double): integer;
begin
  Result := Add(TKmlCoordinate.Create);
  with Last do
  begin
    Latitude := ALatitude;
    Longtitude := ALongtitude;
    Altitude := AAltitude;
  end;
end;

Теперь, так как было сказано выше, что LineString и LinearRing практически ничем друг от друга не отличаются, я создал ещё один класс, который будет выступать родителем для линий:

TKmlLine = class(TKmlGeometry)
  private
    FAltitudeOffset: double;
    FTessellate: boolean;
    FCoordinates: TKmlCoordinateList;
  public
    constructor Create; override;
    destructor Destroy; override;
    function Save(AParentNode: IXMLNode): IXMLNode; override;
    function AddCoord(ALongtitude, ALatitude, AAltitude: double): integer;
    //свойства общие для линий
    property AltitudeOffset: double read FAltitudeOffset write FAltitudeOffset;
    property Tessellate: boolean read FTessellate write FTessellate;
    property Coordinates: TKmlCoordinateList read FCoordinates;
  end;

Метод Save у класса выглядит следующим образом:

function TKmlLine.Save(AParentNode: IXMLNode): IXMLNode;
var FS: TFormatSettings;
begin
  if Self is TKmlLineString then
  begin
    //У LineString должно быть не менее двух точек
    if FCoordinates.Count < 2 then
      raise EKmlException.Create(rsLineStringError);
    Result := inherited CreateNode('LineString', AParentNode, nil);
  end
  else
    if Self is TKmlLinearRing then
  begin
    //у LinearRing должно быть не менее четырех точек
    if FCoordinates.Count < 4 then
      raise EKmlException.Create(rsLinearRingPointCountError);
    //первая и последняя точки должны совпадать
    if not FCoordinates[0].IsEqual(FCoordinates[Pred(FCoordinates.Count)]) then
      raise EKmlException.Create(rsLinearRingError);
    Result := inherited CreateNode('LinearRing', AParentNode, nil);
  end;
  //записываем в XML-узел общие для линий свойства
  with Result do
  begin
    AddChild('extrude').Text := BoolStrs[Extrude];
    AddChild('tessellate').Text := BoolStrs[Tessellate];
    AddChild('altitudeMode').Text := AltitudeModeStr[AltitudeMode];
    FS.DecimalSeparator := '.';
    AddChild('altitudeOffset').Text := FloatToStr(AltitudeOffset, FS);
    AddChild('coordinates').Text := Coordinates.ToString;
  end;
end;

Метод AddCoord — это «обёртка» для AddCoordinate списка координат:

function TKmlLine.AddCoord(ALongtitude, ALatitude, AAltitude: double): integer;
begin
  Result := FCoordinates.AddCoordinate(ALongtitude, ALatitude, AAltitude);
end;

Использование класса TKmlLine позволяет нам написать без особых проблем оба класса для создания линий в kml:

//рисует незамкнутую линию
TKmlLineString = class(TKmlLine)
  private
    FDrawOrder: integer;
  public
    function Save(AParentNode: IXMLNode): IXMLNode; override;
    property DrawOrder: integer read FDrawOrder write FDrawOrder;
  end;
 
function TKmlLineString.Save(AParentNode: IXMLNode): IXMLNode;
begin
  Result:=inherited Save(AParentNode);
  Result.AddChild('drawOrder').Text := DrawOrder.ToString;
end;
 
//рисует замкнутую линию
TKmlLinearRing = class(TKmlLine)
  public
    function Save(AParentNode: IXMLNode): IXMLNode; override;
    procedure CloseLine;//замыкает линию
  end;
 
function TKmlLinearRing.Save(AParentNode: IXMLNode): IXMLNode;
begin
  Result:=inherited Save(AParentNode);
end;
 
procedure TKmlLinearRing.CloseLine;
begin
  if FCoordinates.Count < 3 then
    raise EKmlException.Create(rsCloseLineError);
  FCoordinates.Add(TKmlCoordinate.Create);
  FCoordinates.First.AssignTo(FCoordinates.Last);
end;

Проверим, как будут работать наши классы в нашем демонстрационном приложении из прошлой статьи. Добавим вот такой код в программу:

//добавим в документ незамкнутую линию
ADocument.Placemark.Add(TKmlPlacemark.Create(TKmlLineString));
  with ADocument.Placemark.Last do
  begin
    //добавляем свойства, характерные для всех "фич"
    Name := 'Линия';
    Snippet := 'Описание';
    Visibility := True;
    //задаем свойства линии
    TKmlLineString(Geometry).Extrude := False;
    TKmlLineString(Geometry).AltitudeMode := TAltitudeModeEnum(cbAltitudeModeEnum.ItemIndex);
    TKmlLineString(Geometry).AddCoord(-122.366278, 37.818844, 30);
    TKmlLineString(Geometry).AddCoord(-122.365248, 37.819267, 30);
    TKmlLineString(Geometry).AddCoord(-122.365640, 37.819861, 30);
    TKmlLineString(Geometry).AddCoord(-122.366669, 37.819429, 30);
  end;
end;

Выполнение этого кода приведет к тому, что на карте Google Earth будет нарисована незамкнутая линия, состоящая из четырех точек и, при этом, линия будет прижата к земле (extrude=false):

Так как мы ещё не разбирались с тем, как изменять цвета объектов, скажу, что красный цвет и толщину линии я задавал непосредственно в Google Earth, а вообще код «рисует» белую линию толщиной в 1 пиксель.
Проверим, что получится, если «выдавить» линию над поверхностью. Перепишем код для LineString следующим образом:

TKmlLineString(Geometry).Extrude := True; //выдавливаем линию
TKmlLineString(Geometry).AltitudeMode := TAltitudeModeEnum.amAbsolute;//высота точки - абсолютное значение
TKmlLineString(Geometry).AddCoord(-122.366278, 37.818844, 10);
TKmlLineString(Geometry).AddCoord(-122.365248, 37.819267, 30);
TKmlLineString(Geometry).AddCoord(-122.365640, 37.819861, 20);
TKmlLineString(Geometry).AddCoord(-122.366669, 37.819429, 30);

Этот код приведет к рисованию в Google Earth вот такой замысловатой фигуры:

Таким образом, манипулируя свойством Extrude и координатами высоты точек можно выстраивать на карте «стены», «заборы» и так далее. Теперь попробуем нарисовать замкнутую линию по этим же четырем точкам, сохраняя режим «выдавливания», то есть код теперь будет вот таким:

  ADocument.Placemark.Add(TKmlPlacemark.Create(TKmlLinearRing));
  with ADocument.Placemark.Last do
  begin
    Name := edPlaceName.Text;
    Snippet := edPlaceSnippet.Text;
    Description := memPlaceDescr.Text;
    Visibility := True;
 
    TKmlLinearRing(Geometry).Extrude := True;
    TKmlLinearRing(Geometry).AltitudeMode := TAltitudeModeEnum.amAbsolute;
    TKmlLinearRing(Geometry).AddCoord(-122.366278, 37.818844, 10);
    TKmlLinearRing(Geometry).AddCoord(-122.365248, 37.819267, 30);
    TKmlLinearRing(Geometry).AddCoord(-122.365640, 37.819861, 20);
    TKmlLinearRing(Geometry).AddCoord(-122.366669, 37.819429, 30);
    TKmlLinearRing(Geometry).CloseLine; //замыкаем линию
  end;
end;

В результате получим вот такую фигуру на карте:

Что касается полигонов (Polygon), то их рисование чуть-чуть по-сложнее, чем рисование линий, так как эти фигуры должны иметь как минимум одну внешнюю границу и одну или несколько внутренних границ. Класс полигона в Delphi выглядит следующим образом:

TKmlPolygon = class(TKmlGeometry)
  private
    FTessellate: boolean;
    FOuterBoundaryIs: TKmlLinearRing;
    FInnerBoundaryIs: TObjectList;
  public
    constructor Create; override;
    destructor Destroy; override;
    function Save(AParentNode: IXMLNode): IXMLNode; override;
    property Tessellate: boolean read FTessellate write FTessellate;
    property OuterBoundaryIs: TKmlLinearRing read FOuterBoundaryIs;
    property InnerBoundaryIs: TObjectList read FInnerBoundaryIs;
  end;
FInnerBoundaryIs это список-дженерик (TObjectList), который содержит объекты TKmlLinearRing
Метод Save будет вот таким:
function TKmlPolygon.Save(AParentNode: IXMLNode): IXMLNode;
var
  BoundaryNode: IXMLNode;
  I: integer;
begin
  if FInnerBoundaryIs.Count = 0 then
    raise EKmlException.Create(rsPolygonError);
  Result := inherited CreateNode('Polygon', AParentNode, nil);
  with Result do
  begin
    AddChild('extrude').Text := BoolStrs[Extrude];
    AddChild('tessellate').Text := BoolStrs[Tessellate];
    AddChild('altitudeMode').Text := AltitudeModeStr[AltitudeMode];
    BoundaryNode := AddChild('outerBoundaryIs');
    FOuterBoundaryIs.Save(BoundaryNode);
    BoundaryNode := AddChild('innerBoundaryIs');
    for I := 0 to Pred(FInnerBoundaryIs.Count) do
      FInnerBoundaryIs[I].Save(BoundaryNode);
  end;
end;

Посмотрим, как использовать этот класс в Delphi. Для начала, нарисуем полигон, как было показано на рисунке выше (с вырезанной областью). В Delphi это делается вот так:

  ADocument.Placemark.Add(TKmlPlacemark.Create(TKmlPolygon));
  with ADocument.Placemark.Last do
  begin
    Name := edPlaceName.Text;
    Snippet := edPlaceSnippet.Text;
    Description := memPlaceDescr.Text;
    Visibility := True;
 
    TKmlPolygon(Geometry).Extrude := False;
    TKmlPolygon(Geometry).AltitudeMode := TAltitudeModeEnum.amClampToGround;
    //добавляем внешнюю границу
    TKmlPolygon(Geometry).OuterBoundaryIs.AddCoord(-122.366278, 37.818844, 30);
    TKmlPolygon(Geometry).OuterBoundaryIs.AddCoord(-122.365248, 37.819267, 30);
    TKmlPolygon(Geometry).OuterBoundaryIs.AddCoord(-122.365640, 37.819861, 30);
    TKmlPolygon(Geometry).OuterBoundaryIs.AddCoord(-122.366669, 37.819429, 30);
    TKmlPolygon(Geometry).OuterBoundaryIs.CloseLine;
    //добавляем одну внутреннюю границу
    TKmlPolygon(Geometry).InnerBoundaryIs.Add(TKmlLinearRing.Create);
    TKmlPolygon(Geometry).InnerBoundaryIs.Last.AddCoord(-122.366212, 37.818977, 30);
    TKmlPolygon(Geometry).InnerBoundaryIs.Last.AddCoord(-122.365424, 37.819294, 30);
    TKmlPolygon(Geometry).InnerBoundaryIs.Last.AddCoord(-122.365704, 37.819731, 30);
    TKmlPolygon(Geometry).InnerBoundaryIs.Last.AddCoord(-122.366488, 37.819402, 30);
    TKmlPolygon(Geometry).InnerBoundaryIs.Last.CloseLine;
  end;
end;

Результат выполнения кода:

Нарисуем «выдавленный» полигон:

    //выдавливаем полигон
    TKmlPolygon(Geometry).Extrude := True;
    TKmlPolygon(Geometry).AltitudeMode := TAltitudeModeEnum.amAbsolute;
 
    TKmlPolygon(Geometry).OuterBoundaryIs.AddCoord(-122.366278, 37.818844, 30);
    TKmlPolygon(Geometry).OuterBoundaryIs.AddCoord(-122.365248, 37.819267, 30);
    TKmlPolygon(Geometry).OuterBoundaryIs.AddCoord(-122.365640, 37.819861, 30);
    TKmlPolygon(Geometry).OuterBoundaryIs.AddCoord(-122.366669, 37.819429, 30);
    TKmlPolygon(Geometry).OuterBoundaryIs.CloseLine;
 
    TKmlPolygon(Geometry).InnerBoundaryIs.Add(TKmlLinearRing.Create);
    TKmlPolygon(Geometry).InnerBoundaryIs.Last.AddCoord(-122.366212, 37.818977, 30);
    TKmlPolygon(Geometry).InnerBoundaryIs.Last.AddCoord(-122.365424, 37.819294, 30);
    TKmlPolygon(Geometry).InnerBoundaryIs.Last.AddCoord(-122.365704, 37.819731, 30);
    TKmlPolygon(Geometry).InnerBoundaryIs.Last.AddCoord(-122.366488, 37.819402, 30);
    TKmlPolygon(Geometry).InnerBoundaryIs.Last.CloseLine;

Результат выполнения кода:

Таким образом, сегодня мы научились использовать три основных примитива для рисования геометрических объектов с использованием kml — линию, замкнутую линию и полигон. Как видите, трех этих элементов kml хватает, чтобы рисовать самые замысловатые фигуры на картах Google Earth. Осталось только поделиться с вами исходником модуля :)

Скачать модуль вы можете, как обычно, со страницы исходников Delphi из раздела Google API

Демку выложу позже, когда добавлю в неё новые возможности по рисованию.

Книжная полка

Описание Подробно рассматривается библиотека FM, позволяющая создавать полнофункциональное программное обеспечение для операционных систем Windows и OS X, а также для смартфонов и планшетных компьютеров, работающих под управлением Android и iOS
купить книгу delphi на ЛитРес
Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
купить книгу delphi на ЛитРес
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
купить книгу delphi на ЛитРес
5 2 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
0 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии