Table of Contents

MapServer

Pacchetti installati su Debian Sarge:

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:

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:

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.