Table of Contents
MapServer
Pacchetti installati su Debian Sarge:
- cgi-mapserver
- mapserver-bin
- mapserver-doc
- perl-mapscript
- python-mapscript
- php4-mapscript (da pkg-grass.alioth.debian.org, non c'è in testing.)
Una introduzione: MapServer 5.x Tutorial.
CGI MapServer
Lo script mapserv
è l'eseguibile CGI del pacchetto MapServer, accetta parametri GET e POST e viene usato come motore per generare singole immagini o complesse pagine web interattive. Si può fare un test per vedere se MapServer funziona in questo modo: preparare un MapFile di nome /var/www/hello-world.map
con il seguente contenuto:
MAP NAME HELLO STATUS ON EXTENT 0 0 4000 3000 SIZE 400 300 IMAGECOLOR 255 200 255 WEB IMAGEPATH "/tmp/" IMAGEURL "/tmp/" END LAYER NAME "credits" STATUS DEFAULT TRANSFORM FALSE TYPE ANNOTATION FEATURE POINTS 200 150 END TEXT 'Hello World!' END CLASS LABEL TYPE BITMAP COLOR 0 0 0 END END END END
Con il browser si richiede la pagina http://host/cgi-bin/mapserv?map=/var/www/hello-world.map&mode=map
. Lo script CGI genera al volo l'immagine, in questo caso un box 400 x 300 pixel con un'etichetta di testo al centro.
In questo caso lo script CGI ha ricevuto come parametri GET il nome del file .map
che definisce l'immagine da generare e il parametro mode=map
che indica di restituire semplicemente l'oggetto bitmap, senza la pagina interattiva predefinita (mode=browse
).
Parametro map nell'URL
Non è bello che nell'URL compaia per esteso la path del mapfile, sia per un discorso di sicurezza (si rivela la struttura interna del filesystem), sia per evitare di essere dipendenti da una particolare installazione. Tramite opportune variabili di ambiente è possibile modificare il comportamento di MapServer.
Le variabili di ambiente possono essere impostate tramite la direttiva SetEnv
di Apache, ad esempio inserita in una sezione Directory
.
<Directory "/usr/lib/cgi-bin"> AllowOverride None Options ExecCGI -MultiViews +SymLinksIfOwnerMatch Order allow,deny Allow from all # CGI-MapServer environment variables: # Check mapfiles againts this path: #SetEnv MS_MAP_PATTERN "^/usr/local/lib/cgi-mapserver/" # Disallow mapfile full path, use env vars instead. SetEnv MS_MAP_NO_PATH "true" # To use this mapfile, put map=MAP_ORTOFOTO2011 into the URL: SetEnv MAP_ORTOFOTO2011 "/usr/local/lib/cgi-mapserver/ortofoto2011.map" </Directory>
MS_MAP_PATTERN | Con questa direttiva il parametro map ricevuto dal CGI è il percorso del mapfile, ma viene validato rispetto ad una espressione regolare. Si impedisce di poter scegliere arbitrariamente il mapfile da remoto. |
---|---|
MS_MAP_NO_PATH | Se questa variabile è istanziata, il parametro map non viene interpretato come percorso del mapfile, ma come il nome di una variabile di ambiente che a sua volta indica il mapfile. |
MAP_ORTOFOTO2011 | Esempio di variabile di ambiente che punta al mapfile. |
MapFile
Un MapFile è indispensabile per generare una mappa: definisce gli oggetti che verranno visualizzati e altri dettagli della mappa stessa (dimensioni, colori, ecc.). Gli oggetti sono organizzati in strati (layer) che vengono sovrapposti. La sintassi di un MapFile è spiegata nella MapFile Reference. Qui abbiamo l'indice della documentazione.
Anche il sito umn.mapserver.ch ha una buona guida sui mapfile.
MapFile con parametri
È possibile passare a run-time alcuni parametri al mapfile, tramite i parametri della richiesta inviata al CGI-BIN. Vedere la pagina Run-time Substitution della documentazione.
Ad esempio è possibile definire un MAP.LAYER.FILTER
di questo tipo:
FILTER ("multimedia = '%multimedia%' and seats >= %nseats% and sound = '%sound%')
Nella query CGI sarà sufficiente aggiungere i parametri multimedia=yes&seats=100&sound=yes
. Non tutti i parametri del mapfile accetta la sostituzione di variabile, ecco l'elenco:
- LAYER.DATA
- LAYER.TILEINDEX
- LAYER.CONNECTION
- LAYER.FILTER
- CLASS.EXPRESSION
Se il prametro viene usato per comporre una query SQL (come nell'esempio precedente) questa possibilità espone al rischio di SQL injection. Per evitare questo rischio si definiscono i pattern di validazione per ciascuna variabile. Contestualmente è possibile definire dei valori di default per le variabili che non fossero presenti nella query:
METADATA 'multimedia_validation_pattern' '^yes|no$' 'sound_validation_pattern' '^yes|no$' 'nseats_validation_pattern' '^[0-9]{1,3}$' 'default_sound' 'yes' 'default_nseats' '5' 'default_multimedia' 'yes' END
Parametri nella query su attributi
Quando si effettua una query sugli attributi di un layer, nella richiesta CGI-BIN è possibile includere alcuni parametri per filtrare il risultato della ricerca:
mode | Se vale itemquery seleziona una sola feature, con itemnquery seleziona tutte le feature. |
---|---|
qlayer | Obbligatorio: il nome del layer da interrogare. |
qstring | Obbligatorio: la clausola di selezione. Ad esempio idgpx=156 . Per un layer PostgreSQL si tratta di una clausola WHERE. |
qitem | Facoltativo: limita il risultato della ricerca ad un solo attributo. |
Il layer deve essere interrogabile, cioè deve comprendere un LAYER.TEMPLATE
valido. Per evitare attacchi di tipo SQL injection bisogna obbligatoriamente definire una regola di validazione per la QSTRING (comprensiva di eventuali spazi, parentesi, ecc.):
METADATA 'qstring_validation_pattern' '^idgpx=[0-9]+$' END
MapFile collegato a PostGIS
In questo caso gli oggetti da visualizzare nella mappa sono contenuti in un database PostGIS. Abbiamo la tabella waypoints
, nella colonna wpt
ci sono dei oggetti di tipo POINT
memorizzati in coordinate latitudine/longitudine. Per ogni punto viene visualizzato un simbolo (cerchio) e un'etichetta presa dal campo descr
del record. Viene anche specificata una clausola WHERE idtype = 3
per limitare il numero di punti visualizzati. L'estensione della mappa EXTENT
è espressa in gradi di latitudine e longitudine. Si deve fornire nella directory font
il file con il font .ttf e la mappa nomi-font…
Vedere anche questa mail molto esplicativa.
MAP EXTENT 10 43 12 44 IMAGETYPE "png" SIZE 640 480 IMAGECOLOR 200 255 200 FONTSET "fonts/fonts.txt" OUTPUTFORMAT NAME "png" DRIVER "GD/PNG" MIMETYPE "image/png" IMAGEMODE RGB EXTENSION "png" FORMATOPTION "interlace=on" END SYMBOL NAME "circle" TYPE ellipse FILLED true POINTS 5 5 END END LAYER NAME 'waypoints' TYPE point STATUS default CONNECTIONTYPE postgis CONNECTION "user=strade password=****** dbname=strade host=127.0.0.1" DATA "wpt from waypoints" FILTER "idtype = 3" LABELITEM "descr" CLASS STYLE SYMBOL "circle" COLOR 255 255 255 OUTLINECOLOR 240 0 0 END LABEL POSITION auto SIZE 8 COLOR 0 0 0 TYPE truetype FONT arial END END END END
Se si vuole aggiungere un layer vettoriale (linee), supponendo di avere la tabella tracksegments
che contiene il campo trkseg
di tipo LINESTRING
, si può aggiungere una sezione di questo tipo:
LAYER NAME 'tracksegments' TYPE line STATUS default CONNECTIONTYPE postgis CONNECTION "user=strade password=****** dbname=strade host=127.0.0.1" DATA "trkseg from tracksegments" CLASS STYLE COLOR 0 0 0 END END END
Infine proviamo ad aggiungere un layer vettoriale contenente poligoni, supponendo di avere la tabella countries
che contiene il campo poly
di tipo POLYGON
. Notare che i layer vengono sovrapposti uno dopo l'altro, quindi in generale il layer dei poligoni deve essere dichiarato prima del layer che contiene i punti e le linee. Quindi si può aggiungere prima degli altri layer una sezione di questo tipo:
LAYER NAME 'polygon' TYPE polygon STATUS default CONNECTIONTYPE postgis CONNECTION "user=strade password=****** dbname=strade host=127.0.0.1" DATA "poly from countries" FILTER "idcountry = 39" CLASS STYLE COLOR 255 255 255 OUTLINECOLOR 35 137 232 END END END
Calusola DATA del mapfile
La sintassi della clausola DATA
può essere più evoluta di quelle viste sopra, ad esempio può contenere una SELECT
oppure può specificare l'indice univoco o lo SRID (sistema di riferimento) da utilizzare. Fare attenzione alle maiuscole e minuscole!
DATA "the_geom from (SELECT * FROM ...) as foo using unique field_id using SRID=4326"
Specificare lo using unique
è indispensabile se la query o view sottostante non ha il campo oid
. Tale campo esisteva implicitamente nelle tabelle (ma non nelle view) con versioni di PostgreSQL precedenti alla 8.1. Il costruttore di query del MapServer ha bisogno di un indice univoco e se non specificato espressamente utilizza il campo oid
.
Specificare la clausola using SRID
evita a MapServer l'onere di una chiamata alla funzione find_srid()
per determinare automaticamente il sistema di riferimento della geometria.
Mappa interattiva con un MapServer Template
Se vogliamo una mappa interattiva (cliccabile) invece di una semplice immagine statica si utilizza la modalità mode=browse
del cgi-bin (invece del mode=map
). In tal caso nel file .map è necessario specificare il percorso di un file .html (il template) che funzionerà da contesto dentro il quale l'immagine mappa viene mostrata:
TEMPLATE '/var/www/default/maps/geocaches.html'
La richiesta al server web sarà quindi qualcosa del tipo: http://host/cgi-bin/mapserv?map=/var/www/maps/mappa.map&mode=browse.
Il file .html deve avere un'opportuna struttura e deve contenere degli speciali segnaposto che MapServer interpreta e sostituisce con gli attuali valori. Ad esempio se il codice html contiene le seguenti righe:
<input type="hidden" name="map" value="[map]"> <input type="hidden" name="imgext" value="[mapext]"> <input type="hidden" name="scale" value="[scale]"> <input type="hidden" name="imgxy" value="[center]">
quando viene fatta la richiesta al server web, il contenuto generato al volo sarà qualcosa del tipo:
<input type="hidden" name="map" value="/var/www/maps/geocaches.map"> <input type="hidden" name="imgext" value="1777697.577948 4101470.140269 3222674.575580 5185202.888493"> <input type="hidden" name="scale" value="6400000.000628"> <input type="hidden" name="imgxy" value="320.0 240.0">
La pagina html può contenere dei campi che influenzano il comportamento dell'immagine in seguito al click (pan, zoom-in, zoom-out). Per maggiori dettagli ed esempi su come si cotruisce un template .html per MapServer, vedere la guida di riferimento ai template.
PHP-MapScript
Si tratta di una estensione del linguaggio PHP che trasforma le funzionalità di MapServer in oggetti del linguaggio. In pratica - invece di generare le mappe a partire da file .map, che sono inevitabilmente poco flessibili - si definiscono degli oggetti nel linguaggio PHP, si assegnano le opportune proprietà ad essi (grosso modo equivalenti alle direttive che si impostano nei file .map) e alla fine si genera l'immagine corrispondente. In questo modo la generazione della mappa è interamente controllata dall'applicativo web che la richiede.
L'unione di questo linguaggio e della tecnologia AJAX consente di creare applicativi web estremamente interattivi ed efficaci. Esempi significativi sono p.mapper, ka-Map, OpenLayers.
La configurazione di un server con Apache + MapScript + pMapper non è semplicissima, soprattutto se si vuole stare attenti alla sicurezza. Veder in proposito la pagina MapScript Filesystem Hierarchy and Permissions.
Qui di seguito un estratto che genera una mappa minimale e la salva in un file su disco. Il file può essere eventualmente servito al client tramite readfile().
// Create the new MapServer MAP object. $map = ms_newMapObj(''); $map->outputformat->set('driver', 'GD/PNG'); $map->outputformat->set('extension', 'png'); $map->outputformat->set('transparent', MS_ON); $map->set('width', $mapfile_image_width); $map->set('height', $mapfile_image_height); $map->web->set('imagepath', $ms_imagepath); $map->web->set('imageurl', $ms_imageurl); $map->setextent($xmin - $margin, $ymin - $margin, $xmax + $margin, $ymax + $margin); // Add the only one layer. $layer = ms_newLayerObj($map); $layer->setConnectionType(MS_POSTGIS); $layer->set('name', 'bnd'); $layer->set('type', MS_LAYER_POLYGON); $layer->set('status', MS_DEFAULT); $layer->set('connection', $DB_CONNECT); $layer->set('data', 'bnd FROM vmap0_polbnda using unique id and using SRID=4326'); // Add some colors. $class = ms_newClassObj($layer); //$class->setexpression("([id] < 100)"); //$class->setexpression("([tile_id] = 11)"); $class->setexpression("('[nam]' eq 'TOSCANA')"); $style = ms_newStyleObj($class); // For a MS_LAYER_LINE, this is the color of the line. // For a MS_LAYER_POLYGON, this is the color of the filling. $style->color->setRGB(0, 255, 255); // For a MS_LAYER_POLYGON, this is the color of the outline. $style->outlinecolor->setRGB(255, 0, 0); // Not used by MS_LAYER_LINE and MS_LAYER_POLYGON? $style->backgroundcolor->setRGB(255, 0, 0); // Save the image into the MapServer IMAGEPATH. $image = $map->draw(); $image_url = $image->saveWebImage('MS_PNG',1,1,0); // Save the image in other directory (useful for permanent storage). $image->saveImage($image_filename, $map);
Alcune note su setexpression(): serve a determinare quando una classe (e soprattutto gli stili associati) vengono usati per gli oggetti di un determinato layer. La sintassi per l'espressione è alquanto astrusa: i nomi degli attributi dell'oggetto geografico (dallo shapefile o dal database come in questo esempio) vanno tra parentesi quadrate, però se si tratta di espressioni stringa vanno racchiuse tra apici. Il tutto va racchiuso tra parentesi tonde. La sintassi completa è documentata nel manuale dell'oggetto class.
Purtroppo non esiste un metodo per assegnare colori diversi in base ad un attributo dell'oggetto, si devono definire tante classi per quanti sono i colori da utilizzare e definire altrettante setexpression()
che selezionino gli oggetti a cui assegnare il colore.
MapScript su GRASS/PostGIS via OGR
MapScript può attingere ai dati GRASS tramite le librerie OGR. GRASS può mantenere gli attributi di un vettoriale dentro un database PostGIS. Normalmente le credenziali usate da GRASS per accedere al database sono memorizzati nel file $HOME/.grasslogin6
.
Per fare in modo che questo meccanismo di autenticazione venga usato anche da MapScript (cioè dal PHP), devono essere soddisfatte le seguenti condizioni:
- Il file
.grasslogin6
deve essere leggibile dall'utente del server web (es. www-data in Debian). - La variabile d'ambiente
HOME
deve puntare alla directory che contiene il file.
Normalmente la variabile HOME
non è impostata durante l'esecuzione del PHP, la si può impostare con la funzione putenv()
. Nell'esempio seguente viene eseguito il programma ogrinfo su un file che a sua volta richiama GRASS e PostGIS, di fatto utilizzando le credenziali in .grasslogin6
:
putenv("HOME=/var/www"); system("ogrinfo -ro /home/niccolo/grass/Toscana/PERMANENT/vector/t_fiumi_principali/head");
ATTENZIONE: impostare le variabili d'ambiente in PHP potrebbe essere vietato se è attivo il PHP safe mode.
Una modo per impostare la variabile d'ambiente per tutta un'applicazione PHP senza dover modificare tutti i sorgenti potrebbe essere questa:
creare un file .htaccess
con questo contenuto:
php_value auto_prepend_file env.php
creare poi il file env.php
con questo contenuto:
<? putenv('HOME=/var/www') ?>
Proiezioni nei file .map
I layer presenti in un file .map possono essere memorizzati in datum diversi, per sovrapporli correttamente è quindi opportuno specificare (nel mapfile) il sistema di coordinate usato da ciascun layer. Omettendo queste indicazioni si possono produrre errori grossolani se ad esempio si uniscono dati la cui latitudine/longitudine è espressa in gradi (ad esempio usando il sistema WGS84 comune agli apparecchi GPS) con dati espressi in metri (ad esempio se si utilizza una delle proiezioni UTM). Oppure gli errori possono essere minimi con discostamenti di pochi metri.
La mappa che viene generata ha anch'essa la sua proiezione, che in generale può essere diversa da quelle utilizzate dai layer che la compongono.
I parametri che interessano in queste circostanze sono i seguenti:
MAP # Proiezione utilizzata per disegnare la mappa. # Qui usiamo la proiezione Monte Mario / Italy zone 2 PROJECTION "init=epsg:3004" END # In questa proiezione l'unità di misura sono i metri, # parametro indispensabile per ottenere una SCALEBAR corretta. UNITS meters # Per determinare esattamente la zona compresa nella mappa. # Ci si deve esprimere nella corretta unità di misura (metri in # questo caso) e rispetto al sistema di riferimento scelto. EXTENT 1777697 4101470 3222674 5185202 ... LAYER NAME 'Confini regionali' TYPE polygon ... # Questi dati sono nel datum NAD83. PROJECTION "init=epsg:4269" END ...
Debug query su PostGIS
Una mappa basata su una view di Postgres risultava particolarmente lenta ad essere eseguita. E' risultato che MapServer utilizza un cursore per effettuare la query. Attivando l'opzione log_statement = true
in /etc/postgresql/postgresql.conf
, si è visto che la query era qualcosa del tipo:
BEGIN; DECLARE mycursor BINARY CURSOR FOR SELECT toponimo, wpt FROM wpt_comuni_view WHERE ( wpt && setSRID('BOX3D(4.83 36, 20.16 47.5)'::BOX3D, 4326) ); FETCH ALL IN mycursor;
Utilizzando la semplice SELECT il risultato era quasi istantaneo, ma con il CURSOR invece c'era un'attesa di oltre un minuto (su circa 8000 record). E' risultato che il query planner di Postgres, nel caso del cursore, utilizzava un algoritmo teso a restituire in poco tempo solo una piccola percentuale dei record (questo è in genere lo scopo dei cursori), mentre il FETCH ALL risultava assolutamente penalizzato.
Non potendo intervenire sul costruttore di query del MapServer si è risolto aggiungendo una clausola ORDER BY alla VIEW sottostante. In questo modo il query planner viene indotto ad utlizzare un algoritmo più adatto allo scopo. Vedere in proposito questa mail.