Tekninen artikkeli

GeoPDF Delphissä: Geospatiaaliset koordinaatit PDFlibPas-kirjaston avulla

Useimmat kehittäjät pitävät PDF-sivua paperiarkkina, jossa on tekstiä ja kuvia. Georeferoitu PDF on enemmän kuin se. Se kantaa tarpeeksi tietoa ottaakseen pisteen sivulta, mitattuna tavallisissa sivuyksiköissä, ja ilmoittaakseen leveysasteen ja pituusasteen, jonka päällä se sijaitsee todellisessa maailmassa. Tämä yksi tosiasia on se, mikä muuttaa PDF:n käyttökelpoiseksi kantoaalloksi topografiselle kartalle, kiinteistömittauskartalle, tulvavyöhykenäyttelylle tai mille tahansa GIS-viennille, joka on tulostettava ja joka silti merkitsee jotain. Geometria on tiedostossa; ainoa kysymys on, lukeeko lataajasi sen

Syy siihen, miksi tämä jää huomaamatta, on se, että GeoPDF avautuu ja tulostuu täsmälleen samalla tavalla kuin mikä tahansa muu PDF. Mikään renderöidyllä sivulla ei ilmoita, että kartta on rekisteröity koordinaattijärjestelmään. Rekisteröinti elää sanakirjoissa (dictionaries), jotka roikkuvat sivuoliosta, eikä niitä koskaan piirretä, ja katseluohjelma, joka sivuuttaa ne, näyttää sinulle kartan yhtä kaikki. Tehdäksesi tiedoston kanssa mitään paikkatietoon liittyvää, kuten koordinaattien lukemia, uudelleenprojektioita tai asetteluja muiden tasojen päälle, sinun on käytävä nämä sanakirjat läpi itse

Luonnossa elää kaksi standardia

Lukijan, joka haluaa käsitellä todellisen maailman tiedostoja, on selviydyttävä kahdesta georekisteröintijärjestelmästä, koska molemmat ovat liikkeessä ja tietty tiedosto voi käyttää kumpaa tahansa. Vanhempi on OGC 08-139r2:ssä kuvattu OGC-koodaus, joka kiinnittää LGIDict:n (geospatiaalinen rekisteröintisanakirja) sivulle. Se on ajalta ennen mitään ISO-siunausta ja oli de facto -formaatti varhaiselle GeoPDF-tulosteelle, joten suuri osa vanhoista kartoista kantaa sitä eikä mitään muuta

Nykyaikainen järjestelmä on ISO-standardoitu malli asiakirjassa ISO 32000-1 §8.8.2. Yhden sivutason sanakirjan sijaan se mallintaa geospatiaalista dataa sivun Viewport (näkymä) -alueena, johon on kiinnitetty Measure (mitta) -sanakirja, ja mittasanakirja nimeää maantieteellisen koordinaattijärjestelmän. Tämä on koodaus, jota Acrobat ja nykyiset GIS-viejät kirjoittavat. Vankka tuoja (importer) tarkistaa molemmat: lukee näkymät ISO-mallia varten ja siirtyy (tai lisäksi tarkastaa) LGIDictin tiedostoille, jotka sisältävät vain vanhan rekisteröinnin

Näkymät (Viewports) ja niiden rajat

ISO-mallissa georekisteröinnin yksikkö on näkymä (viewport), ja sivulla voi olla useita näkymiä. Suuri arkki voi sijoittaa pääkartan yhteen suorakulmioon, eri mittakaavassa olevan lisäkartan toiseen ja selitepaneelin, jota ei ole georeferoitu lainkaan. Jokaisella näkymällä on BBox, suorakulmio sivulla, jota näkymä hallitsee, joten lukija tietää, mihin osaan arkkia tietty koordinaattijärjestelmä pätee. Katseluohjelma päättää, mitä mittasanakirjaa käytetään testaamalla napsautettua pistettä näitä laatikoita vastaan (hit-testing)

PDFlibPas paljastaa valitun sivun näkymät suoraan. GetPageViewPortCount palauttaa, kuinka monta niitä on, GetPageViewPortID muuttaa yhdestä alkavan indeksin ViewPortID-kahvaksi, ja GetViewPortBBox lukee rajoittavan suorakulmion yksi ulottuvuus kerrallaan. Dimension-argumentti valitsee, minkä reunan tai laajuuden haluat: 0 on Vasen, 1 on Ylä, 2 on Leveys, 3 on Korkeus, 4 on Oikea ja 5 on Ala

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 describe the map area for this viewport
    end;
  finally
    Pdf.Free;
  end;
end;

Jos GetPageViewPortID palauttaa nollan ViewPortID:ksi, se tarkoittaa, että kyseisessä indeksissä olevaa näkymää ei löytynyt, joten tarkista se ennen kahvan (handle) välittämistä eteenpäin

Mittaussanakirjan sisällä

Geometria, joka rekisteröi sivun maailmaan, elää näkymään (viewport) liitetyssä mittasanakirjassa. GetViewPortMeasureDict palauttaa MeasureDictID:n tietylle ViewPortID:lle, tai nollan, kun näkymällä ei ole mittasanakirjaa, mikä on normaali tapaus selitteelle tai otsikkopaneelille. Mittasanakirja sisältää kolme asiaa, jotka on syytä lukea: koordinaattijärjestelmät, joihin se viittaa, taulukot, jotka sitovat sivupisteet maantieteellisiin pisteisiin, ja yksikön, jossa pistetieto ilmaistaan

Itse rekisteröinti on kaksi rinnakkaista taulukkoa. GPTS on maantieteellisten pisteiden taulukko, maantieteellisessä koordinaattijärjestelmässä annetut leveysaste- ja pituusasteparit. LPTS on sivutilapisteiden taulukko, joka ilmaistaan murto-osina näkymän BBoxista, jotta ne selviävät skaalauksesta. LPTS:n kohde n ja GPTS:n kohde n nimeävät saman fyysisen sijainnin, kerran sivukoordinaateissa ja kerran maapallolla. Kolme tai useampi tällainen pari kiinnittää affiinin tai yleisessä tapauksessa projektiivisen muunnoksen, joka kartoittaa minkä tahansa sivukoordinaatin näkymän sisällä maailmankoordinaatiksi. Niiden lukeminen on kyse molempien taulukoiden läpikäymisestä samassa tahdissa

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 holds lat/lon pairs; LPTS holds the matching page fractions.
    // Both arrays are read with one-based item indices.
    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) on the page corresponds to (lat, lon) on the ground
      Inc(j, 2);
    end;
  end;
end;

Mittasanakirja raportoi myös sen näyttöyksiköt toiminnon GetMeasureDictPDU kautta, joka ottaa UnitIndexin 1 lineaarista, 2 pinta-alaa tai 3 kulmayksikköä varten ja palauttaa koodin, joka tunnistaa tietyn yksikön, esimerkiksi metrin tai kansainvälisen jalan lineaarisessa kategoriassa. Bounds-taulukko, joka luetaan komennolla GetMeasureDictBoundsItem, kuvaa nelikulmion näkymän sisällä, jonka mittaus todellisuudessa kattaa, mikä ei aina ole koko suorakulmio

WKT vastaan EPSG

GPTS:n leveys- ja pituusasteet ovat merkityksettömiä tietämättä mihin maantieteelliseen koordinaattijärjestelmään ne kuuluvat, sillä koordinaatti 51.5, -0.1 laskeutuu eri fyysiseen paikkaan WGS 84:n alla kuin vanhemman kansallisen datumin (datum) alla. Mittasanakirja vastaa tähän koordinaattijärjestelmäsanakirjan (coordinate system dictionary) kautta, joka saavutetaan toiminnolla GetMeasureDictGCSDict maantieteellistä järjestelmää varten. PDF kuvaa tämän järjestelmän jollakin kahdesta keskenään vaihdettavasta tavasta, ja lukijan on hyväksyttävä kumpi tahansa

Ensimmäinen on WKT, Well-Known Text, itsenäinen merkkijono, joka määrittelee datumin, ellipsoidin, nollameridiaanin ja yksiköt kokonaisuudessaan. Se on monisanainen, mutta yksiselitteinen eikä tarvitse ulkoista hakutaulukkoa. Toinen on EPSG-koodi, yksi kokonaisluku, joka indeksoi koordinaattijärjestelmän EPSG-rekisterissä; 4326 on WGS 84, kehys, jota suurin osa kuluttajien GPS-tiedoista käyttää. EPSG on kompakti, mutta olettaa lukijan pystyvän ratkaisemaan koodin tietokannasta. Tiedostot esiintyvät yhdellä, toisella tai molemmilla, minkä vuoksi sovellusliittymä (API) paljastaa kaikki kolme: GetCSDictType, GetCSDictEPSG ja GetCSDictWKT. GetCSDictType raportoi, onko järjestelmä maantieteellinen (GEOGCS, palautusarvo 1) vai projisoitu (PROJCS, palautusarvo 2), mikä antaa sinun tulkita loput oikein ennen kuin luotat siihen

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);   // e.g. 4326 for WGS 84, 0 if absent
    wkt    := Pdf.GetCSDictWKT(gcsID);    // full text description, '' if absent
    // Prefer EPSG when present; fall back to parsing WKT otherwise.
  end;
end;

Vanhan LGIDictin lukeminen

Tiedostot, jotka edeltävät viewport-mallia, tai jotka ovat työkaluilla tuotettuja, jotka edelleen lähettävät vanhempaa koodausta, pitävät rekisteröintiään sivun LGIDictissä mittasanakirjan sijaan. PDFlibPas raportoi, kuinka monta tällaista sanakirjaa sivulla on GetPageLGIDictCount:n kautta ja palauttaa kunkin raakasisällön GetPageLGIDictContent-toiminnolla, indeksoituna ykkösestä alkaen. Palautettu teksti on sanakirja sellaisenaan kirjoitettuna, pitäen sisällään OGC 08-139r2 rekisteröintikentät, jotka koodisi sitten jäsentää palauttaakseen samanlaisen sivulta maailmaan -kartoituksen (page-to-world mapping) kuin mittasanakirja tarjoaa. Kirjoituspuolella AddLGIDictToPage kiinnittää LGIDictin nykyiselle sivulle, jotta muuntaja (converter) voi viedä vanhan lomakkeen läpi (round-trip), kun vanha kuluttaja (consumer) edelleen odottaa sitä

var
  lgiCount, k: Integer;
  dictText: WideString;
begin
  lgiCount := Pdf.GetPageLGIDictCount;
  for k := 1 to lgiCount do
  begin
    dictText := Pdf.GetPageLGIDictContent(k);
    // dictText carries the OGC 08-139r2 registration to parse
  end;
end;

Lukemisen kokoaminen yhteen

Täydellinen tuoja (importer) käsittelee kahta järjestelmää parina läpikulkuna jokaisen sivun yli. Valitse sivu, pyydä GetPageViewPortCount ISO-näkymille, ja vetäise jokaiselle näkymälle, joka omistaa mittasanakirjan, sen BBox, GPTS- ja LPTS-taulukot, pistetietoyksikkö (point data unit) ja GCS-kuvaus koordinaattijärjestelmäsanakirjan (coordinate system dictionary) kautta. Tarkista sitten GetPageLGIDictCount minkä tahansa vanhan rekisteröinnin varalta, jota viewport-vaihe ei kattanut. Kartan, joka sisältää molemmat, tulisi olla yhtä mieltä niiden kesken; kartta, joka sisältää vain toisen, ratkeaa edelleen, koska katsoit molemmista paikoista. Matkan varrella palautetut kahvat, ViewPortID, MeasureDictID, CSDictID, ovat yksinkertaisia kokonaislukuja, jotka pysyvät voimassa asiakirjan lataamisen aikana, joten koko kävely on muutama sisäkkäinen silmukka sivuluettelon yli, eikä muistinvarausta (allocation) tarvitse hallita

Kun pystyt palauttamaan rekisteröinnin, sivusta tulee datalähde kuvan sijaan. Seuraavat tekniikat loppuosan sivun lukemiseen on käsitelty pdflibpas-text-image-font-extraction.html-artikkelissa tekstin, kuvan ja fonttien purkamisesta, ja georeferoidun arkin renderöinti laitteelle näytöllä tapahtuvaa mittausta varten on kuvattu pdflibpas-print-preview-device-context.html-läpikäynnissä tulostus- ja esikatselu laitekontekstista. Tässä kuvattu geospatiaalinen lukija toimitetaan osana losLab PDF Library -kirjastoa Delphille ja C++Builderille, muiden tässä blogissa käsiteltyjen lataus-, purku- ja renderöintisovellusliittymien ohella