====== 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: [[http://biometry.gis.umn.edu/tutorial/index.html|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''.
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"
^ ''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 **[[http://www.mapserver.org/mapfile/index.html|MapFile Reference]]**. Qui abbiamo l'**[[http://www.mapserver.org/documentation.html|indice della documentazione]]**.
Anche il sito **[[http://umn.mapserver.ch/|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 **[[http://mapserver.org/cgi/runsub.html|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 [[using_postgis_with_mapserver|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:
quando viene fatta la richiesta al server web, il contenuto generato al volo sarà qualcosa del tipo:
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 [[http://mapserver.gis.umn.edu/docs/reference/templatereference/templatereference|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 [[http://en.wikipedia.org/wiki/Ajax_%28programming%29|AJAX]] consente di creare applicativi web estremamente interattivi ed efficaci. Esempi significativi sono [[http://pmapper.sourceforge.net/|p.mapper]], [[http://ka-map.maptools.org/|ka-Map]], [[http://www.openlayers.org/|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_hierarchy]].
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 **[[http://mapserver.gis.umn.edu/docs/reference/mapfile/class|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 [[slow_queries_from_mapserver_and_qgis|mail]].