niedziela, 24 lutego 2013

Tymczasowa warstwa wektorowa w QGIS

Ważnym elementem filtrowania stanowisk według wybranych kryteriów jest ich wyświetlenie na mapie, choćby po to, by przeanalizować ich rozproszenie w przestrzeni. W QAZP2 jest używana tymczasowa warstwa, gdzie QgsVectorDataProvider jest zaimplementowany w ten sposób, że pobiera obiekty prosto z pamięci, a nie z pliku SHP, albo przestrzennej bazy danych.

Tworzenie tymczasowej warstwy wektorowej razem z zawartością jest dość proste i składa się z następujących kroków:
  1. Utworzenie pustej warstwy wektorowej przez wywołanie konstruktora klasy w następujący sposób: QgsVectorLayer(typGeometryczny, nazwa, 'memory'), gdzie wartość typGeometryczny powinna mieć na przykład wartość 'Point', albo 'Polygon', czy 'Line'. 
  2. Rozpoczęcie edycji przez wywołanie metody QgsVectorLayer.startEditing().
  3. Określenie atrybutów warstwy przez podanie ich listy. Każdy z nich jest reprezentowany przez klasę QgsField. W warstwie tymczasowej można używać tylko następujące typy pól: QVariant.String, QVariant.Double oraz QVariant.Int
  4. Dodanie obiektów QgsFeature, które mają być wyświetlane na warstwie.
  5. Zatwierdzenie edycji.
Funkcja qgsop.tempWarstwa() jest przykładem implementacji powyższego algorytmu.

niedziela, 17 lutego 2013

Golang : obsługa bazy Spatialite

W QAZP2 pojawiła się nowa funkcja, moim zdaniem jedna z ważniejszych, czyli filtrowanie danych. Póki co można ją stosować tylko do stanowisk i polega na wybieraniu z listy stanowisk tych, które spełniają wybrane kryteria. A więc w pierwszym kroku wyszukujemy stanowiska znajdujące się na przykład na pewnym obszarze, a następnie spośród nich stanowiska, które mają dużą wartość dla badań nad przeszłością i wobec których istnieje zagrożenie zniszczenia. To oczywiście tylko przykład, bo praktycznie stanowiska można filtrować w oparciu o większość atrybutów edytowanych w programie. A prezentację sposobu stosowania tego mechanizmu można obejrzeć na krótkim filmie.

To by było na tyle, jeżeli chodzi o ostanie wydarzenia, bo dzisiaj chciałbym dokończyć temat z poprzedniego tygodnia, czyli wykorzystanie języka Go do realizacji projektu QAZP2. Wtedy omówiłem kilka interesujących właściwości Go, które odróżniają go od dobrze znanych C/C++ a także popularnych języków obiektowych, na przykład Javy. Go (Golang) charakteryzuje się bogatą biblioteką standardową, która umożliwia realizację podstawowych zadań, bez konieczności instalacji dodatkowych zależności. W pozostałych przypadkach można się posłużyć intuicyjnym i wygodnym mechanizmem zarządzania nimi. Na przykład do pobrania sterownika do bazy SQLite/Spatialite. Istnieje kilka alternatywnym projektów, w ramach których jest on rozwijany, ja zdecydowałem się na Go-Sqlite3. Jak już pisałem do jego instalacji wystarczy wykonać polecenie
go get github.com/mattn/go-sqlite3
, które po pierwsze pobierze kod źródłowy utrzymywany w repozytorium GIT i po drugie skompiluje go, jeżeli spełnione są wszystkie zależności. To jest proste nie tylko w teorii, z tym że w normalnej postaci sterownika nie można używać do baz przestrzennych. Wynika to z tego, że przed wykonaniem jakiegokolwiek polecenia SQL, które dotyczy danych geometrycznych należy załadować rozszerzenie spatialite, na przykład stosując polecenie (na linuksie) select load_extension('libspatialite.so.3'). Do tego, by ono zadziałało trzeba wcześniej zmodyfikować kod źródłowy sterownika w ten sposób, by w chwili "łączenia się" z bazą umożliwiał ładowanie rozszerzeń, która to możliwość jest domyślnie zablokowana.
Dowolny program może korzystać z bazy SQLite przy pomocy biblioteki funkcji, których używa się na przykład do otwierania bazy, wykonywania poleceń SQL i pobierania wyników. Wśród nich jest także funkcja sqlite3_enable_load_extension, która służy (jak sama nazwa wskazuje ...) do włączenia możliwości ładowania rozszerzeń. Trzeba ją wykonać po otworzeniu pliku z bazą danych. A więc w pliku sqlite3 , wewnątrz funkcji Open, przed deklaracją zwracającą wskaźnik do nowego połączenia z bazą należy wstawić następujące polecenie: 
C.sqlite3_enable_load_extension(db, C.int(1)). I to wszystko. Po otworzeniu bazy zostanie wywołana ww. funkcja, gdzie parametrami są wskaźnik do bazy i liczba 1 oznaczająca True. Po nawiązaniu połączenia wystarczy wykonać polecenie SQL, które spowoduje załadowanie rozszerzenia Spatialite i od tej chwili można przetwarzać dane geometryczne zapisane w przestrzennej bazie danych.

sobota, 9 lutego 2013

Golang : import danych AZP

Jednym z ważniejszych aspektów związanych z projektem QAZP było przygotowanie narzędzia, które (w miarę) bezboleśnie pozwoli na "przerzucenie" danych utworzonych w programie AZPMAX do relacyjnej bazy SQL. Oczywiście pierwszą decyzją był wybór języka programowania, który z wiadomych względów padł na Jave, choćby z tego powodu, że mam sporo doświadczeń związanych z tworzeniem narzędzi obsługujących zarówno DBF-y jak i bazy danych. Niestety musiałem z tego zrezygnować, bo mimo wcześniejszych pozytywnych wyników, tym razem sterownik do SQLite odmówił współpracy z danymi przestrzennymi. Przy próbie inicjalizacji rozszerzenia Spatialite JVM przestawała działać. W związku z tym zdecydowałem się stworzyć narzędzie do importu w języku Golang, który od kilku lat jest rozwijany przez dobrze znaną firmę Google. Ten wybór też ma uzasadnienie, biorąc pod uwagę, że:
  1. mam już pewne doświadczenie związane z tworzeniem oprogramowania w Go,
  2. dobra obsługa baz sql, w tym Sqlite.
Na minus trzeba zaliczyć brak obsługi DBF-ów, ale to póki co rozwiązałem kopiując dane z plików CSV, a do tego formatu można łatwo wyeksportować DBF, posługując się na przykład arkuszem kalkulacyjnym.
Golang / Go ma kilka interesujących właściwości, które przyniosły mu już dużo rozgłosu i powodują, że język dość szybko zyskuje na popularności. Jest statycznie typowany, ale określenie typu zmiennej można przerzucić na kompilator. Wydaje mi się, że nie można go uznać za niskopoziomowy, bo nie umożliwia na przykład wykorzystywanie assemblera, natomiast różnorakie porównania wykazują, że w wielu przypadkach szybkością dorównuje programom pisanym w C / C++. Nad tymi dwoma ma przewagę (przec co niektórych uznawaną za wadę) automatycznego zarządzania pamięcią (garbage collector). Programy można tworzyć w modelu strukturalnym - do reperentowania danych używa się list, map, i struktur bardzo podobnych do tych stosowanych w C / C++, albo takim, który oferuje abstrakcję i polimorfizm (ale bez dziedziczenia) znany z paradygmatu programowania obiektowego. Realizuje się to przy pomocy tak zwanych interfejsów, które nieco przypominają konstrukcje o tej samej nazwie, które stosuje się w Javie. W Golang interfejs jest pewnym kontraktem, który umożliwia tworzenie polimorficznych obiektów. Natomiast inny jest sposób ich stosowania. O ile w Javie, jawnie trzeba podać, że klasa jest implementacją, o tyle w przypadku Golang jest to automatycznie rozpoznawane przez kompilator. Każdy typ danych, dla którego zaimplementowano funkcje wskazane w definicji interfejsu staje się automatycznie jego implementacją. W narzędziu do importu, o którym mowa został zdefiniowany interfejs Tabela z jedną metodą Params, która zwraca tablicę wartości dowolnego typu. Każda struktura, która implementuje interfejs może być zastosowana w metodzie dodaj(ps *sql.Stmt, t Tabela, spr bool), która wykonując metodę Params pobiera wartości ze struktury w takiej kolejności, które powinny być zastosowane jako parametry polecenia SQL reprezentowanego przez inny interfejs Stmt zdefiniowany w bibliotece standardowej Golang.
W tym krótkim opisie trudno wskazać wszystkie właściwości Go, ja skupiłem się tylko na tych, które były istotne z uwagi na problem, który musiałem rozwiązać. W następnym poście opisałem, jak "zmusić" sterownik Go do obsługi danych geometrycznych zapisanych w przestrzennej bazie danych.

niedziela, 3 lutego 2013

Implementacja nowych sygnałów Qt w Pythonie

Dzisiaj będzie krótko, w ramach uzupełnienia do poprzedniego artykułu. Opisałem w nim krótko koncepcję obsługi zdarzeń w Qt4, natomiast teraz chciałbym to uzupełnić o definiowanie nowych sygnałów dla widgetów tworzonych w Pythonie.
Zasada jest dość prosta, bo opiera się na stosowaniu metody pyqtSignal(args, name=nazwa_sygnalu), której zadaniem jest utworzenie nowego sygnału.

Przykład (tworzenie nowego sygnału)

from PyQt4.QtCore import pyqtSignal
from PyQt4.QtGui import QWidget, QPushButton

class NowyWidget(QWidget):
    
    def __init__(self, parent):
        QWidget.__init__(self, parent)
        btn = QPushButton('Kliknij')
        btn.clicked.connect(self.btnClick)

    widgetSygnal = pyqtSignal(str, name='widgetSygnal')

    def btnClick(self, ch):
        self.widgetSygnal.emit('Witaj')

def nowySlot(wiadomosc):
    print wiadomosc

def main():
    wgt = NowyWidget()
    wgt.widgetSygnal.connect(nowySlot)

Ze zdarzeniem QPushButton.clicked został połączony slot w postaci metody btnKlik zdefiniowany w klasie NowyWidget. Gdy użytkownik kliknie w przycisk, zostanie ona uruchomiona, a następnie wyemitowany nowy sygnał widgetSygnal, który został utworzony przy pomocy wspomnianej funkcji pyqtSignal. Zgodnie z jej wywołaniem slot, który podłączymy do sygnału powinien przyjmować jeden argument typu str, tak jak metoda nowySlot, która wyświetla otrzymaną wiadomość na ekranie.

czwartek, 31 stycznia 2013

Obsługa zdarzeń w Qt 4/PyQt

Na codzień tworzę aplikacje w Javie i to przez używanie SWING-a ukształtowało się moje podejście do obsługi zdarzeń, które mają miejsce w komponentach graficznych wyświetlanych użytkownikom. Stąd, kiedy zaczynałem swoją przygodę z Qt 4 i PyQt pewną niespodzianką był mechanizm obsługi zdarzeń, który zastosowano w tej bibliotece. Odbiega od tego z Javy, gdzie robi się to za pomocą tak zwanych listenerów, czyli klas implementujących pewien interfejs definiujący metodę, która jest wywoływana jako rezultat zdarzenia. Na przykład kliknięcie na przycisk powoduje między innymi uruchomienie metody actionPerformed z interfejsu ActionListener, którego implementację rejestruje się dla każdego przycisku.
W przypadku Qt stosuje się tak zwane sygnały (signals) i sloty (slots). Zasada jest tutaj dość prosta - komponent graficzny - na przykład przycisk QPushButton w momencie, gdy zostanie kliknięty emituje sygnał, którego etykieta wygląda następująco clicked(bool). Takie sformułowanie oznacza, że w reakcji na kliknięcie przycisku do podanego slotu zostanie przekazana wartość logiczna (typ bool). Łatwo się domyślić, że rolę slotu pełni inna metoda, która w reakcji na zdarzenie powinna wykonać jakąś operację. Jedynym wymaganiem, jakie musi spełniać slot, to akceptować taką samą liczbę parametrów, tego samego typu, jak to jest zdefiniowane dla sygnału, z którym jest związany. W opisywanym przypadku metoda - slot ma zdefiniowany jeden parametr, przez który będzie przekazana wartość logiczna.
Jednak aby wystąpiła reakcja na zdarzenie, trzeba wybrany sygnał połączyć ze zdefiniowanym slotem. Kontynuując przykład z przyciskiem i sygnałem clicked(bool) utwórzmy metodę klikniecie(zaznaczony). Gdy połączymy sygnał clicked i metodę klikniecie, to w chwili, gdy użytkownik naciśnie za pomocą myszy przycisk, zostanie wywołana metoda klikniecie, a parametr zaznaczony otrzyma wartość logiczną True albo False. Jeżeli tworzymy interfejs przy pomocy PyQt to istnieje kilka sposobów na zdefiniowanie połącznia:
  1. stosując metodę QObject.connect(QObject nadajnik, QtCore.SIGNAL(etykieta), QObject odbiornik, QtCore.SLOT(etykieta)). Nadajnik to obiekt - źródło sygnału, natomiast odbiornik, to obiekt, w którym znajduje się metoda - slot. SIGNAL i SLOT to dwie metody, które służą do określenia sygnału i slotu. Robi się to podając ich etykiety zapisane w postaci łańcucha znaków. Na przykład QObject.connect(przycisk, SIGNAL("clicked(bool)"), poleTxt, SLOT("klikniecie(zaznaczony)")).
  2. Drugi ze sposobów, to pewne uproszczenie pierwszej i polega na tym, że zamiast podawać wprost obiekt - odbiornik i etykietę metody, można bezpośrednio wskazać jej referencję, czyli: QObject.connect( przycisk, SIGNAL("clicked(bool)"), poleTxt.klikniecie).
  3. Wreszcie ostatni ze sposobów, najbliższy konwencji tworzenia programów w Pythonie. Sygnały w PyQt zostały zaimplementowane w ten sposób, że każdy z nich jest także obiektem. Możemy się więc odwołać do niego bezpośrednio i wskazać docelową metodę, która powinna być wywołana w reakcji na zdarzenie: przycisk.clicked.connect(poleTxt.klikniecie).
Poniższy kod prezentuje krótki program, w którym zastosowano wszystkie opisane wcześniej metody obsługi zdarzeń w Qt 4 / PyQt
    class Program(object):
        def __init__(self):
            przycisk = QPushButton("Przycisk")
            QObject.connect(przycisk, SIGNAL("clicked(bool)"), self, SLOT("klikniecie(zaznaczony)"))
            QObject.connect(przycisk, SIGNAL("clicked(bool)"), self.klikniecie))
            przycisk.clicked.connect(self.klikniecie)

        def klikniecie(self, zaznaczony):
            print 'klikniecie przycisku ', zaznaczony

W kolejnym artykule opisałem zasadę tworzenia sygnałów Qt w Pythonie.

wtorek, 22 stycznia 2013

Obsługa plików binarnych w Qt i Sqlite3

Jednym z elementów, które obowiązkowo muszą znaleźć się na karcie KEZA, wypełnianej dla każdego stanowiska w ramach badań Archeologicznego Zdjęcia Polski, jest mapa przedstawiająca jego położenie i ewentualnie kształt (jeżeli taki określono). W QAZP2 to zadanie jest realizowane w ten sposób, że plik zawierający mapę w formacie PNG, JPEG i innych obsługiwanych przez Qt po wskazaniu przez użytkownika jest zapisywany w bazie danych, tej samej, gdzie przechowywane są pozostałe informacje o badaniach. A dokładnie w tabeli MEDIA, gdzie oprócz samego pliku dodawane są jeszcze takie informacje jak jego format, oryginalna nazwa, a także tabelę, z którą jest związany rekord (np. STANOWISKA, TRASY, itd.). Z kolei informacja, z którego dokładnie stanowiska dotyczy określony plik jest zapisana w tabeli ST_MEDIA, gdzie wprowadzony jest identyfikator rekordu z tabeli MEDIA oraz identyfikator z tabeli STANOWISKA, tworząc tym samym połączenie między tymi relacjami. 
W bazach SQLITE do zapisu danych binarnych zapisuje się pola o typie BLOB. Przy pomocy klasy QImage obrazek jest odczytywany z pliku wskazanego przez użytkownika, a następnie zamieniany na sekwencję bajtów przez wczytanie do bufora QBuffer. Z kolei z tego dane są pobierane jako tablica bajtów QByteArray, którą zamieniamy na ciąg znaków. Taka procedura jest konieczna, gdyż pozwala w ostatecznym efekcie uzyskać bufor ale w reprezentacji, którą obsługuje sterownik bazy danych. Taki obiekt można już przekazać jako parametr w poleceniu SQL. Przebieg opisanej procedury można prześledzić w funkcji media.zapiszMapa(). Odczytywanie danych binarnych z bazy i konwertowanie ich na obiekt, który można wydrukować albo po prostu wyświetlić na ekranie przy pomocy Qt jest dużo prostsze. Po pierwsze za pomocą metody QByteArray.fromRawData(dane) konwertuje się wartość pobraną z pola BLOB na wspominaną już tablicę bajtów. Następnie wywołanie metody QImage.fromData(tablica) spowoduje utworzenie obiektu QImage na podstawie bajtów znajdujących się w tablicy utworzonej przed momentem.

wtorek, 15 stycznia 2013

Budowanie wykazów nazw

Ostatnimi czasy dużo się działo w kwestii QAZP2. Ostatnie poprawki błędów, niekiedy dość poważnych, nowe funkcjonalności - na przykład wykonywanie skryptów modyfikujących zawartość bazy danych - co może mieć znaczenie, gdy osoba odpowiadająca za techniczny aspekt bazy połączonej z QAZP2 nie ma do niej bezpośredniego dostępu. W takim wypadku wystarczy spreparować skrypt SQL i wysłać do użytkownika, który za pomocą interfejsu graficznego aplikuje przygotowane zmiany. Rzecz jasna taki mechanizm jest pewnym zagrożeniem dla stanu bazy danych, jednak jest to koszt, który można ponieść, pamiętając, by nie stosować skryptów z niepewnego źródła. Między innymi z tego powodu trudno było mi znaleźć czas i spełnić obietnicę, że nowe artykuły będą wprowadzane conajmniej raz na tydzień.

Ostatnimi czasy na blogu była poruszana kwestia wykazów pojęć archeologicznych, których używa się do klasyfikacji kulturowo-funkcjonalno-historycznej. W QAZP2 wykorzystywane są do tego wykazy przygotowane w Narodowym Instytucie Dziedzictwa i uzupełnione o nazwy, których obecności wymaga praktyka badań archeologicznych. Ich konstrukcję próbowałem opisać w poprzednim artykule, natomiast dzisiaj chciałbym przedstawić inne podejście, które chociaż nie zostało wdrożone, to wydaje się ciekawą alternatywą dla wyżej wspomnianego sposobu budowania wykazów.