Thursday, March 12, 2015

Ancora un utile widget, per gestire informazioni geografiche - parte 2

Ora che il database sqlite è stato realizzato, possiamo dedicarci al nuovo widget. Per questo piccolo progetto utilizzo ancora la libreria Tkinter, disponibile nell'installazione di base di Python (nulla vieta di usare altre librerie, che sicuramente offrono un'interfaccia meno scarna, come le wx o le qt, ma Tkinter è facile da usare e pronto da subito, per cui lo preferisco).

L'utilità di questo widget in se è, come vedremo, limitata, ma si presta ottimamente all'uso nell'ambito di un progetto più ampio. Ne verificheremo in seguito una possibile applicazione.

Scopo del widget è l'estrazione, a richiesta dell'utilizzatore, delle coordinate geografiche e della timezone di un qualsiasi luogo abitato della superficie terrestre, presente nel database localita.db, unitamente alle informazioni ulteriori che possono essere utili per risolvere omonimie e fornire dettagli utili.

L'immagine a sinistra puo' dare un'idea del funzionamento del widget, ospitato, insieme al widget calendario, di cui ho presentato il progetto in un precedente post, in un frame più grande.

Il funzionamento è molto semplice: è sufficiente inserire il nome completo della località o alcune lettere consecutive, anche interne al nome, nella casella indicata dal pulsante <-- Substr Search. in tempi molto rapidi la combobox sottostante sarà popolata di tutti i nomi delle località che soddisfano il criterio di ricerca. Di default viene presentata la prima della lista, ma si puo', ovviamente, selezionarne una diversa tra quelle che si presentano quando si clicca sulla freccetta a destra. Nella sottostante casella arancione vengono elencati, in ordine, i campi corrispondenti alla selezione. (I colori sono, naturalmente, configurabili).

Passiamo all'esame del listato, ho chiamato lo script python tkAtlas.py (lo trovate, come sempre, in github, sotto la directory atlas, che contiene tutti gli script per creare il database. Come al solito, riporto il codice in frammenti per commentarlo adeguatamente. Il primo frammento comprende l'inizio e la fine dello script, gli altri sono inseriti in sequenza dopo le prime righe.

from Tkinter import *
import tkFont
import ttk
import sqlite3
import os

class Cities:
    """
    This class is a new widget, aimed to deal with world cities from geoname site
    (see http://download.geonames.org/export/dump/) for details.
    A lot of useful informations are taken from a main archive (allCountries.txt)
    and linked to textual geographical informations from the same site
    (admin1CodesASCII.txt and admin2Codes.txt), then reversed in a sqlite3 db,
    which is read from inside the present program
    """

    def __init__(self, master):
        """
        All the widgets are created in __init__, all significant variables
        are instance variables and can be read from outside
        """
        self.master = master

----------


if __name__ == '__main__':
    root = Tk()
    app = Cities(root)
    root.mainloop()

Lo schema dovrebbe esservi ormai familiare: importazione di moduli, dichiarazione di classe, docstring e metodo __init__ per l'inizializzazione dell'istanza di classe, richiesta, in questo caso, dalle ultime righe dello script, eseguite solo se lo script viene lanciato direttamente anzichè essere importato come modulo da un altro script.

        root_dir = os.path.dirname(os.getcwd())
        for root, dirs, files in os.walk(root_dir, topdown=True):
            for file in files:
                if file.endswith('localita.db'):
                    self.db = os.path.join(root, file)

In queste righe si effettua la ricerca del database. Devo dire che la libreria standard di Python non rende molto facile l'esplorazione dei path, per cui, non volendo usare librerie di terze parti, ho dovuto lavorarci un po'.

Se il database è nella stessa directory dello script principale o in una qualsiasi sottodirectory, viene recuperato il path completo del file e viene messo a disposizione della successiva routine di query sql.

        self.conn = sqlite3.connect(self.db)
        self.c = self.conn.cursor()

Con queste righe si gestisce la connessione ad database sqlite. Ricordo che i metodi per la gestione dei database sqlite sono già incorporati nella libreria standard di Python, è sufficiente importare il modulo relativo con l'espressione import sqlite3, che abbiamo collocato all'inizio dello script.

        self.frame1 = Frame(self.master, background='#006f6f', padx=5, pady=5)
        self.frame1.pack(fill=X, expand=1)
        self.label1 = Label(self.frame1, text='Location')
        self.label1.pack(side=LEFT, anchor='w', fill=X, expand=1)
        self.search = StringVar(self.master)
        self.search.set('')
        self.result = ''
        self.entry1 = Entry(self.frame1, textvariable=self.search)
        self.entry1.pack(side=LEFT, fill=X, expand=1)
        self.button1 = Button(self.frame1, text='<-- Substr. Search', command=self.select)
        self.button1.pack(side=LEFT, fill=X, expand=1)
        self.frame2 = Frame(self.master)
        self.frame2.pack()
        self.combo1 = ttk.Combobox(self.frame2, text=self.result, width=60)
        self.combo1.pack(anchor='w')
        self.combo1.bind('<<ComboboxSelected>>', self.update_label)
        self.label2 = Label(self.frame2, text='\n\n\n\n\n\n\n\n\n', justify=LEFT, anchor='w', width=60, relief=GROOVE, background='orange')
        self.label2.pack(side=LEFT)

In queste righe di codice si creano vari oggetti (widget) per la visualizzazione grafica delle operazioni di query del database. Ho previsto due frame, il primo contiene una label, una entry e un button, il secondo una combobox e una label. La casella di entry è quella che utilizziamo per la stringa di ricerca che, come ho detto sopra, puo' essere il nome completo della località o una sottostringa. Maiuscole e minuscole non sono importanti: SQL è un linguaggio case-insensitive. La variabile wrapper self.search non è usata per il binding di un metodo, ma solo come contenitore della sottostringa di ricerca.

Il button invece, quando viene cliccato, lancia il metodo select che vediamo successivamente. La combobox non è tra i widget standard di Tkinter ma è contenuta nel modulo ttk, che ne è un'estensione, già comunque presente nell'installazione Python di base. Più interessante è il binding che faccio tra la scelta di un item della combobox e il lancio del metodo update_label. Il binding fatto in questo modo puo' essere scelto anche per tutti i casi di un elemento widget non responsivo al click del mouse o alla pressione della tastiera, per esempio il passaggio del mouse su una label o su un'immagine.

Infine ho previsto per la seconda label (quella in arancione) una disposizione incolonnata delle informazioni relative alla località, per ragioni di spazio e di leggibilità. All'inizio è composta di sole righe vuote, viene aggiornata con il metodo che vediamo successivamente.

    def select(self, *args):
        """
        This is the search engine in the database. As a result, the combobox
        and the bottom label texts are updated
        """
        search = self.search.get()
        p_query = ('%' + search + '%',)
        query = 'select nome_ascii, latitudine, longitudine, admin1, country, ascad1, ascad2, popolazione, elevazione, timezone                 from localita where nome_ascii like ? order by nome_ascii'
        self.c.execute(query, p_query)
        self.cities_list = self.c.fetchall()
        self.combo1.config(values='')
        values = [ [x[0],
         x[4],
         x[5],
         x[6]] for x in self.cities_list ]
        for i in range(len(values)):
            for j in range(4):
                values[i][j] = str(values[i][j])

            values[i] = tuple(values[i])

        self.combo1['values'] = tuple(values)
        self.combo1.set(values[0][0] + ' ' + values[0][1] + ' ' + values[0][2])
        index = self.combo1.index(0)
        self.update_label(index)

Questo metodo della classe Cities crea la query in base all'input utente, la esegue e raccoglie in risultati in una lista, che puo' essere esplorata con i metodi consueti. Nella combobox sono riportati solo alcuni valori estratti, cioè il nome in caratteri ascii della località. il paese e la regione (o la principale divisione del territorio secondo le regole del paese. La label invece viene popolata con tutti campi, come si vede nel codice seguente. Per ovviare alla possibile mancanza di informazioni (risultato None della query) ho preventivamente convertito in stringa tutti i risultati, None compreso, per poi rituplizzare il tutto, visto che la combobox accetta solo tuple.

    def update_label(self, *args):
        """
        This routine updates the index value to get back the selected row
        in the query results and subsequently updates the top down label
        """
        if args[0] == 0:
            index = 0
        else:
            index = self.combo1.current()
        _list = list(self.cities_list[index])
        s = [ '{}'.format(x) for x in _list ]
        long_list = 'Name: {0}\nLat: {1}\nLong: {2}\nState: {3} ({4})\n'
        long_list += 'Region: {5}\nProvince: {6}\nPopulation: {7}\n'
        long_list += 'Elevation: {8}\nTimezone: {9}'
        self.label2['text'] = long_list.format(s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9])
        self.parameters = self.cities_list[index]

L'ultimo frammento di codice è relativo alla routine (metodo) di aggiornamento in base ai risultati della query. Come si puo' vedere i parametri di ingresso possono essere in numero variabile, a me interessava che, nel momento in cui scelgo l'indice della combobox, come al termine del metodo select, la routine possa gestire l'informazione. In questo caso l'indice al termine della query è sempre fissato a 0, ma cambia in base alla scelta dell'utente sulla lista presente nella combobox. Nel primo caso l'assegnazione è automatica nel corso dell'esecuzione del metodo select, nel secondo consegue alla scelta fatta dall'utente con il successivo lancio della funzione di callback update_label. Questo metodo è pensato essenzialmente per riempire la label con le informazioni disponibili dopo l'esecuzione della query. Ho pensato, inoltre, di mettere a disposizione questi risultati inserendoli in una lista che potrà essere letta al di fuori della classe, come variabile di istanza. In sostanza i valori contenuti nella riga scelta confluiscono in self.parameters e sono leggibili come variabile di istanza Cities().parameters o, come in questo caso, come app.parameters. Da qui in avanti iniziamo a scrivere un vero e proprio software astrologico, in cui il widget calendario e il widget atlante saranno strumenti fondamentali per la gestione dell'ora e del luogo degli eventi astrologici.

No comments:

Post a Comment

How to create a virtual linux machine with qemu under Debian or Ubuntu with near native graphics performance

It's been a long time since my latest post. I know, I'm lazy. But every now and then I like to publish something that other people c...