Техническа статия

GeoPDF в Delphi: Геопространствени координати с PDF Library for Delphi

Повечето разработчици си представят PDF страницата като лист хартия с текст и картини върху него. Геореферираният PDF е нещо повече от това. Той носи достатъчно информация, за да вземе точка от страницата, измерена в обикновени единици на страницата, и да отчете географската ширина и дължина, над които се намира тя в реалния свят. Този единствен факт превръща PDF файла в използваем носител за топографска карта, кадастрален план, чертеж на наводнена зона или всеки GIS експорт, който трябва да бъде отпечатан и все пак да има смисъл. Геометрията е там във файла, единственият въпрос е дали вашият модул за зареждане я чете

Причината това да се пропуска е, че GeoPDF се отваря и отпечатва точно както всеки друг PDF. Нищо в рендираната страница не съобщава, че картата е регистрирана към координатна система. Регистрацията живее в речници, закачени за обекта на страницата, които никога не се изчертават, а четец, който ги игнорира, ви показва картата по същия начин. За да направите нещо пространствено с файла (отчитане на геодезически координати, репроектиране, наслагване спрямо други слоеве), трябва сами да обходите тези речници

Два стандарта съществуват в практиката

Четец, който иска да обработва файлове от реалния свят, трябва да се справи с две схеми за георегистрация, тъй като и двете са в обращение и даден файл може да използва всяка от тях. По-старата е кодирането на OGC, описано в OGC 08-139r2, което прикачва LGIDict (речник за геопространствена регистрация) към страницата. То предхожда всяко одобрение от ISO и беше де факто форматът за ранните GeoPDF изходни файлове, така че голям обем от наследствени карти съдържа него и нищо друго

Модерната схема е тази, която ISO стандартизира в ISO 32000-1 §8.8.2. Вместо един речник на ниво страница, тя моделира геопространствените данни като Viewport на страницата с прикачен речник Measure, а речникът за измерване указва географска координатна система. Това е кодирането, което записват Acrobat и настоящите GIS експортиращи програми. Надеждният импортер проверява и за двете: чете viewports за ISO модела и се връща към LGIDict (или допълнително го проверява) за файлове, които носят само наследената регистрация

PDF Library for Delphi диаграма, противопоставяща ISO модела за георегистрация с viewport и measure речник на старата схема LGIDict, носена от по-стари GeoPDF файлове
Две схеми за георегистрация се срещат в истински GeoPDF файлове, а страницата може да носи едната, другата или и двете. Всеки път преобразува координатите на страницата в географска ширина и дължина

Viewports и техните граници

В ISO модела единицата за георегистрация е viewport-ът и една страница може да има няколко. Голям лист може да разположи основна карта в един правоъгълник, вмъкната карта с различен мащаб в друг и панел с легенда, който изобщо не е георефериран. Всеки viewport носи BBox – правоъгълникът на страницата, който управлява viewport-ът, така че четецът знае коя част от листа се управлява от дадена координатна система. Проверката на кликната точка спрямо тези полета е начинът, по който четецят решава кой речник за измерване да използва

PDF Library for Delphi излага графичните прозорци (viewports) на избраната страница директно. GetPageViewPortCount връща колко са те, GetPageViewPortID преобразува базиран на единица индекс в дескриптор ViewPortID, а GetViewPortBBox чете ограничаващия правоъгълник по едно измерение наведнъж. Аргументът Dimension избира кой ръб или размер искате: 0 е Left, 1 е Top, 2 е Width, 3 е Height, 4 е Right, и 5 е Bottom

var
  Pdf: TPDFlib;
  vpCount, i, vpID: Integer;
  Left, Top, Width, Height: Double;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('topo_sheet.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Pdf.SelectPage(1);

    vpCount := Pdf.GetPageViewPortCount;
    for i := 1 to vpCount do
    begin
      vpID := Pdf.GetPageViewPortID(i);
      Left   := Pdf.GetViewPortBBox(vpID, 0);
      Top    := Pdf.GetViewPortBBox(vpID, 1);
      Width  := Pdf.GetViewPortBBox(vpID, 2);
      Height := Pdf.GetViewPortBBox(vpID, 3);
      // Left/Top/Width/Height описват картографската област за този viewport
    end;
  finally
    Pdf.Free;
  end;
end;

Стойност ViewPortID, равна на нула, от GetPageViewPortID означава, че viewport-ът на този индекс не може да бъде намерен, така че я проверете, преди да прехвърлите дескриптора нататък

Вътре в речника за измерване (measure dictionary)

Геометрията, която регистрира страницата към света, живее в речника за измерване, прикачен към даден viewport. GetViewPortMeasureDict връща MeasureDictID за даден ViewPortID, или нула, когато viewport-ът няма речник за измерване, което е нормалният случай за легенда или заглавен панел. Речникът за измерване съдържа три неща, които си струва да бъдат прочетени: координатните системи, които реферира, масивите, които свързват точките от страницата с географски точки, и единицата, в която се изразяват данните за точките

Самата регистрация представлява два паралелни масива. GPTS е масивът от географски точки – двойки географска ширина и дължина, дадени в географската координатна система. LPTS е масивът от точки в пространството на страницата, изразени като части от BBox на viewport-а, така че да оцелеят при мащабиране. Елемент n от LPTS и елемент n от GPTS обозначават едно и също физическо местоположение, веднъж в координати на страницата и веднъж на глобуса. Три или повече такива двойки определят афинната (или в общия случай проективната) трансформация, която съпоставя всяка координата на страницата във viewport-а с координата от реалния свят. Четенето им е въпрос на синхронно обхождане на двата масива

var
  measID, gptsCount, lptsCount, j: Integer;
  lat, lon, px, py: Double;
begin
  measID := Pdf.GetViewPortMeasureDict(vpID);
  if measID <> 0 then
  begin
    gptsCount := Pdf.GetMeasureDictGPTSCount(measID);
    lptsCount := Pdf.GetMeasureDictLPTSCount(measID);
    // GPTS съдържа двойки географска ширина/дължина; LPTS съдържа съответните части от страницата.
    // И двата масива се четат с индекси от едно.
    j := 1;
    while j < gptsCount do
    begin
      lat := Pdf.GetMeasureDictGPTSItem(measID, j);
      lon := Pdf.GetMeasureDictGPTSItem(measID, j + 1);
      px  := Pdf.GetMeasureDictLPTSItem(measID, j);
      py  := Pdf.GetMeasureDictLPTSItem(measID, j + 1);
      // (px, py) на страницата отговаря на (lat, lon) на терена
      Inc(j, 2);
    end;
  end;
end;

Речникът за измерване също така отчита своите дисплейни единици чрез GetMeasureDictPDU, което приема UnitIndex със стойност 1 за линейни, 2 за площ или 3 за ъглови единици и връща код, идентифициращ конкретната единица – например метър или международен фут за линейната категория. Масивът Bounds, прочетен с GetMeasureDictBoundsItem, описва четириъгълника във viewport-а, който измерването действително обхваща, което не винаги е целият правоъгълник

WKT срещу EPSG

Географската ширина и дължина в GPTS са безсмислени, без да се знае към коя географска координатна система принадлежат, тъй като координата 51.5, -0.1 попада на различно физическо място при WGS 84 в сравнение с по-стара национална координатна система. Речникът за измерване отговаря на това чрез речник за координатна система, достъпен с GetMeasureDictGCSDict за географската система. PDF описва тази система по един от два взаимозаменяеми начина и четецът трябва да приеме и двата

PDF Library for Delphi: Макет на PDF страница, показващ основен map viewport, вложен viewport и несочена легенда с click hit-тестване срещу BBox на всеки viewport
Всеки viewport управлява само областта вътре в своя BBox, така че hit-testing казва на четеца кой речник за измерване важи. Нулев ViewPortID означава, че заявенят viewport не е намерен

Първият е WKT (Well-Known Text) – самостоятелен низ, който описва подробно координатната система, елипсоида, началния меридиан и единиците в пълния им вид. Той е подробен, но еднозначен и не се нуждае от външна таблица за търсене. Вторият е EPSG код – едно цяло число, което индексира координатна система в регистъра на EPSG; 4326 е WGS 84, рамката, която използва по-голямата част от потребителските GPS данни. EPSG е компактен, но предполага, че четецът може да разреши кода спрямо база данни. Файловете се появяват с единия, другия или и двата варианта, поради което API излага и трите функции: GetCSDictType, GetCSDictEPSG и GetCSDictWKT. GetCSDictType отчита дали системата е географска (GEOGCS, върната стойност 1) или проектирана (PROJCS, върната стойност 2), което ви позволява да интерпретирате останалото правилно, преди да му се доверите

var
  gcsID, csType, epsg: Integer;
  wkt: WideString;
begin
  gcsID := Pdf.GetMeasureDictGCSDict(measID);
  if gcsID <> 0 then
  begin
    csType := Pdf.GetCSDictType(gcsID);   // 1 = GEOGCS, 2 = PROJCS
    epsg   := Pdf.GetCSDictEPSG(gcsID);   // напр. 4326 за WGS 84, 0 ако липсва
    wkt    := Pdf.GetCSDictWKT(gcsID);    // пълно текстово описание, '' ако липсва
    // Предпочитай EPSG, когато е наличен; иначе се връщай към разбор на WKT.
  end;
end;

Четене на наследения LGIDict

Файловете, които предхождат модела viewport, или които са произведени от инструменти, все още излъчващи по-старото кодиране, носят своята регистрация в LGIDict на страницата, а не в речник за измерване. PDF Library for Delphi отчита колко такива речници има дадена страница чрез GetPageLGIDictCount и връща обратно необработеното съдържание на всеки от тях с GetPageLGIDictContent, индексирано от едно. Върнатият текст е речникът във вида, в който е записан, съдържащ полетата за регистрация OGC 08-139r2, които вашият код след това анализира, за да възстанови същия вид съпоставяне на страницата към света, каквото предоставя речникът за измерване. От страната на записването, AddLGIDictToPage прикачва LGIDict към текущата страница, така че конверторът може да преработи наследения формат, когато стар потребител все още го очаква

var
  lgiCount, k: Integer;
  dictText: WideString;
begin
  lgiCount := Pdf.GetPageLGIDictCount;
  for k := 1 to lgiCount do
  begin
    dictText := Pdf.GetPageLGIDictContent(k);
    // dictText носи регистрацията по OGC 08-139r2 за разбор
  end;
end;

Пълно сглобяване на четенето

Пълният импортер третира двете схеми като двойка преминавания над всяка страница. Изберете страницата, поискайте от GetPageViewPortCount броя на ISO viewports и за всеки viewport, който притежава речник за измерване, изтеглете неговия BBox, неговите масиви GPTS и LPTS, неговата мерна единица за данни за точки и описанието на GCS чрез речника за координатна система. След това проверете GetPageLGIDictCount за всяка наследена регистрация, която преминаването през viewport-а не е обхванало. Карта, която носи и двете регистрации, трябва да показва съгласие между тях; карта, която носи само една, все пак се разрешава, тъй като сте проверили и на двете места. Дескрипторите, върнати по пътя – ViewPortID, MeasureDictID, CSDictID, са обикновени цели числа, които остават валидни, докато документът е зареден, така че цялото обхождане представлява няколко вложени цикъла над списъка със страници без необходимост от управление на заделена памет

PDF Library for Delphi: Сдвоени фракции в странично пространство LPTS и масиви latitude longitude GPTS, съединени елемент по елемент, за да фиксират афинното преобразуване страница-към-свят
Елемент n на LPTS и елемент n на GPTS назовават едно и също физическо местоположение веднъж на страницата и веднъж на глобуса. Три или повече двойки фиксят афинното или проективното преобразуване

След като можете да възстановите регистрацията, страницата се превръща в източник на данни, а не просто в картина. Свързаните техники за четене на останалата част от страницата са разгледани в статията за извличане на текст, изображения и шрифтове, а изобразяването на георефериран лист на устройство за измерване на екрана е описано в ръководството за устройство за печат и предварителен преглед. Геопространственият четец, описан тук, се доставя като част от losLab PDF Library за Delphi и C++Builder, заедно с API за зареждане, извличане и рендиране, разгледани на други места в този блог