http://www.paolocorti.net/2006/09/20/mapserver-tutorial-for-c-mapscript-asp-net/
MapServer Tutorial for C# mapscript (ASP .NET)
Posted by Paolo Corti on September 20,2006
This tutorial will try to guide you step by step in implementing a GIS (Geographic Information Systems) web solution,based on Open Source software (MapServer),within the .NET Framework.
The tutorial is designed to work with Visual Studio 2003 and .NET 1.1,but you will easily be able to perform all the necessary steps to complete it using another version of Visual Studio (ie: Visual Studio 2005) or other IDEs (like Visual Web Developer 2005 Express Edition or even the notepad). It will work also using .NET 2 as c# mapscript is .NET 2 compatible.
The tutorial is focused to people with knowledge of C# (but can be easily implemented with .NET Visual Basic) and the .NET framework,and with some background in GIS. If you are new to GIS,a good place to start is here.
This tutorial could be easily adapted for other languages and frameworks rather than C# (or VB) and Microsoft .NET Framewok. In fact the Open Source component we are going to use (MapServer) is also available for PHP,Java and Python,and other languages.
The whole tutorial is downloadable at this address,where you can find the data (shapefiles) necessary to complete the tutorial (but you can easily adapt your data),and a complete working Visual Studio 2003 solution with this tutorial’s code.
You can take a look at a working online demo of this tutorial here.
As we will implement a C# ASP .NET Application,take care about this issue (that should be solved in the future): MapServer thread safety and about plans to solve this issue.
Index of Tutorial
Introduction
1. Introduction to MapServer Web GIS development environment
2. Installing MapServer
3. Creating the MapFile and data configuration
4. Designing the tutorial user interface
5. Implementing the C# mapscript code
6. Migrating shapefiles to PostGIS
7. Connection MapFile layer to PostGIS
8. Adapting C# code to work indifferently with shapefile or PostGIS layers
And if the tutorial is not working for you (you get compilation errors,maps are not displayed,editing isIntroduction to MapServer Web GIS development environment (c# mapscript tutorial,part 1)
Posted by Paolo Corti on July 2,2006
What is MapServer?
According to MapServer official site:
MapServer is an Open Source development environment for building spatially-enabled internet applications. MapServer is not a full-featured GIS system,nor does it aspire to be. Instead,MapServer excels at rendering spatial data (maps,images,and vector data) for the web.
MapServer was originally developed by the University of Minnesota (UMN) ForNet project in cooperation with NASA and the Minnesota Department of Natural Resources (MNDNR). Presently,the MapServer project is hosted by the TerraSIP project,a NASA sponsored project between the UMN and consortium of land management interests.
The software is maintained by a growing number of developers (nearing 20) from around the world and is supported by a diverse group of organizations that fund enhancements and maintenance.
MapServer main features
The MapServer’s main features are:
Advanced cartographic output
Scale dependent feature drawing and application execution
Feature labeling including label collision mediation
Fully customizable,template driven output
TrueType fonts
Map element automation (scalebar,reference map,and legend)
Thematic mapping using logical- or regular expression-based classes
Support for popular scripting and development environments
PHP,Python,Perl,Ruby,Java,and C#
Cross-platform support
Linux,Windows,Mac OS X,Solaris,and more
A multitude of raster and vector data formats
TIFF/GeoTIFF,EPPL7,and many others via GDAL
ESRI shapfiles,PostGIS,ESRI ArcSDE,Oracle Spatial,MysqL and many others via OGR
Open Geospatial Consortium (OGC) web specifications
WMS (client/server),non-transactional WFS (client/server),WMC,WCS,Filter Encoding,SLD,GML,SOS
Map projection support
On-the-fly map projection with 1000s of projections through the Proj.4 library
A good starting point to get other useful informations about MapServer is here.
How to develop with MapServer
MapServer can be used in two ways,with the CGI Interface or with the MapScript API.
Here I will discuss how to implement a MapServer site with MapScript API,accessed by C# MapScript.
MapScript provides a scripting interface for MapServer for the development of Web (ie: ASP :NET,J2EE,PHP,etc…) and stand-alone applications (ie: Windows .NET,etc…). MapScript is used independently of CGI MapServer (in fact we won’t even install the CGI modality),it is a loadable module that adds MapServer capability to your favorite scripting language. MapScript currently exists in PHP,Tcl,and C# flavors.
All of these MapScript API where created by SWIG.
SWIG is a software development tool that connects programs written in C and C++ with a variety of high-level programming languages. SWIG is used with different types of languages including common scripting languages such as Perl,Ruby and PHP. The list of supported languages also includes non-scripting languages such as C#,Common Lisp (CLISP,Allegro CL,CFFI,UFFI),Modula-3 and OCAML. Also several interpreted and compiled Scheme implementations (Guile,MzScheme,Chicken) are supported. SWIG is most commonly used to create high-level interpreted or compiled programming environments,user interfaces,and as a tool for testing and prototyping C/C++ software. SWIG can also export its parse tree in the form of XML and Lisp s-expressions. SWIG may be freely used,distributed,and modified for commercial and non-commercial use.
For more information about MapScript’s SWIG API independent language you should consult the Reference.not working…),please take a look here
Installing MapServer (c# mapscript tutorial,part 2)
Posted by Paolo Corti on July 3,2006
Download MapServer
Download precompiled binaries here. For the purpose of this tutorial is indifferent if you download the MS4W for PHP4 or PHP5.
(As an alternative you could download an installation toolkit like this and compile yourself MapServer,but it is more difficult).
Installation
All you need to do is to extract the zip file,for example at: C:/ms4w.
Here we will not mention how to install CGI MapServer,because we are using the other modality (MapScript).
After doing that take a look at the structures of the directories created under C:/ms4w.
All the references we need in our C# ASP .NET (or Windows) application are the dll under:
C:/ms4w/Apache/cgi-bin/mapscript/csharp and the dll under C:/ms4w/Apache/cgi-bin.
The only .NET dll is C:/ms4w/Apache/cgi-bin/mapscript/csharp/mapscript_csharp.dll.
For all the other dlls is enough for them to be accessible for our application. To get this it is possible to operate in two ways:
for every .NET project manually copy these dll under bin
or simply put the path to all these dll under PATH Windows environment variable.
Note that not even a dll need to be registered on Windows (with REGSVR32),so when your application will be complete you could easily FTP it to your web server host.
Introduction to MapServer Web GIS development environment (c# mapscript tutorial,and modified for commercial and non-commercial use.
For more information about MapScript’s SWIG API independent language you should consult the Reference.
Designing the tutorial user interface (c# mapscript tutorial,part 4)
Posted by Paolo Corti on July 26,2006
Create an ASP .NET C# Project
Open Visual Studio,create a new ASP .NET C# Project called TutorialMapServer:
Insert application key value in web.config
Open web.config and add the following appSettings section under configuration section:
<configuration>
<appSettings>
<!-- Path to MapFile -->
<add key="mapFilePath"
value = " C:/training/mapServerTutorial/data/csharptutorial.map"
/>
</appSettings>
<!-- ........ -->
</configuration>
For the key mapFilePath be sure to put the right path to the map file (also a relative path is valid).
Create the main web form
Add a WebForm called Default.aspx:
On Default.aspx page add the following controls,like in the picture (use a table and flowlayout):
a button named butRefresh;
a button named butFullExtent;
a button named butClear;
a imageButton named ibMap;
a checkBoxList named cblLayers;
a dropdownlist named ddlLayers;
a label named lblInfo;
a literal named litIdentifyResult;
a radioButtonList named rblGisTools with the following items:
item with value 0,text “Zoom In” and selected true;
item with value 1,text “Zoom Out”Â
item with value 2,text “Identify”
item with value 3,text “Add Point”
Â
For doing so you can copy and paste this HTML code in the HTML View of Default.aspx page in Visual Studio:
<body>
<form id="Form1" method="post" runat="server">
<TABLE id="Table1" cellSpacing="1" cellPadding="1" width="500" border="0">
<TR>
<TD colSpan="3">
<P align="center"><STRONG>C# <STRONG>MapServer </STRONG>Tutorial,by Paolo Corti
(26/07/2006)</STRONG></P>
</TD>
</TR>
<TR>
<TD colSpan="3">
<P align="center">User:
<asp:TextBox id="txtUser" runat="server">Paolo</asp:TextBox></P>
</TD>
</TR>
<TR>
<TD style="HEIGHT: 186px">Layer's visibility (check to make it visible)
<asp:checkBoxlist id="cblLayers" runat="server"></asp:checkBoxlist><asp:button id="butRefresh" runat="server" Text="Refresh Map"></asp:button></TD>
<TD style="HEIGHT: 186px"><asp:imagebutton id="ibMap" runat="server" BorderWidth="1px"></asp:imagebutton></TD>
<TD style="HEIGHT: 186px">Select a GIS action to perform on the Map:
<asp:radiobuttonlist id="rblGisTools" runat="server" Width="93px">
<asp:ListItem Value="0" Selected="True">Zoom In</asp:ListItem>
<asp:ListItem Value="1">Zoom Out</asp:ListItem>
<asp:ListItem Value="2">Identify</asp:ListItem>
<asp:ListItem Value="3">Add Point</asp:ListItem>
</asp:radiobuttonlist>
<asp:button id="butFullExtent" runat="server" Text="Full Extent"></asp:button><br>
<asp:Button id="butClear" runat="server" Text="Clear Active Point Layer"></asp:Button></TD>
</TR>
<TR>
<TD colSpan="3">Select the active layer (to identify):
<asp:dropdownlist id="ddlLayers" runat="server"></asp:dropdownlist></TD>
</TR>
<TR>
<TD colSpan="3"><asp:label id="lblInfo" runat="server" Font-Bold="True" ForeColor="Red"></asp:label></TD>
</TR>
<TR>
<TD colSpan="3">
<asp:Literal id="litIdentifyResult" runat="server"></asp:Literal></TD>
</TR>
</TABLE>
</form>
</body>
Create the MapStream web form
Add a second web form called MapStream.aspx (we will not use controls on this page,we will use it to send an image stream to ibMap,with the map produced by MapServer)
Implementing the C# mapscript code (c# mapscript tutorial,part 5)
Posted by Paolo Corti on July 26,2006
Add reference to mapscript
Add the reference to mapscript_csharp.dll (browse to the mapscrip installation folder,for example C:/ms4w/Apache/cgi-bin/mapscript/csharp):
{{:mapserver:tutorial:add_reference.jpg|:mapserver:tutorial:add_reference.jpg}}
Remember that if you didn’t set PATH environment variable to the MapServer dlls you will need to manually copy them under the bin folder of this ASP .NET application.
The c# code for Default.aspx page
This is the complete code to add for the Default.aspx page:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Data.OleDb; //for dbf connection
using System.IO; //for copying the point shapefile
namespace TutorialMapServer
{
/// <summary>
/// User Interface for c# MapServer Tutorial
/// </summary>
public class _Default : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Literal litIdentifyResult;
protected System.Web.UI.WebControls.DropDownList ddlLayers;
protected System.Web.UI.WebControls.Button butFullExtent;
protected System.Web.UI.WebControls.RadioButtonList rblGisTools;
protected System.Web.UI.WebControls.ImageButton ibMap;
protected System.Web.UI.WebControls.Button butRefresh;
protected System.Web.UI.WebControls.CheckBoxList cblLayers;
protected System.Web.UI.WebControls.Label lblInfo;
protected System.Web.UI.WebControls.TextBox txtUser;
protected System.Web.UI.WebControls.Button butClear;
//private variable for this class
mapObj map;
/// <summary>
/// Page Load of Tutorial User Interface
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Page_Load(object sender,System.EventArgs e)
{
if(!Page.IsPostBack) //First access to the map
{
//send image stream from MapServer to ibMap
ibMap.ImageUrl = "MapStream.aspx?ACTION=INITMAP";
//initialize controls
mapObj map = new mapObj(System.Configuration.ConfigurationSettings.AppSettings["mapFilePath"].ToString());
//iterate the map layer to populate ddlLayer and cblLayer
for(int i=0;i<map.numlayers;i++)
{
layerObj layer = map.getLayer(i);
ddlLayers.Items.Add(layer.name);
cblLayers.Items.Add(layer.name);
//If this condition is true,the layer is visible
if(layer.status==(int)mapscript.MS_ON)
{
cblLayers.Items[i].Selected = true;
}
}
}
else //Next accesses to the map,let's get it from session
{
map = (mapObj)Session["MAP"];
}
}
/// <summary>
/// Click Event on the Map button control
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ibMap_Click(object sender,System.Web.UI.ImageClickEventArgs e)
{
lblInfo.Text = "";
String Action = "";
String activeLayer=ddlLayers.SelectedItem.Text;
//we have to check what GIS tool is needed
switch(rblGisTools.SelectedItem.Text.ToUpper())
{
case "ZOOM IN":
Action = "ZOOMIN";
break;
case "ZOOM OUT":
Action = "ZOOMOUT";
break;
case "IDENTIFY":
Action = "IDENTIFY";
break;
case "ADD POINT":
Action = "ADDPOINT";
break;
}
//For Identify let's call DoIdentify
if(Action.Equals("IDENTIFY"))
{
DoIdentify(e.X,e.Y,activeLayer);
}
//For Add Point let's call AddPoint
if(Action.Equals("ADDPOINT"))
{
String[,] fieldValues = new String[2,2];
fieldValues[0,0]="POI_USER";
fieldValues[0,1]= txtUser.Text;
fieldValues[1,0]="POI_TIME";
fieldValues[1,1]= DateTime.Now.ToShortDateString() + "," + System.DateTime.Now.ToLongTimeString();
AddPoint(e.X,activeLayer,fieldValues);
}
//Stream map image to ibMap according to the needed GIS Action
ibMap.ImageUrl = "MapStream.aspx?ACTION=" + Action + "&X=" + e.X + "&Y=" + e.Y + "&ACTIVELAYER=" + activeLayer;
}
/// <summary>
/// Create a full Extent Map
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void butFullExtent_Click(object sender,System.EventArgs e)
{
ibMap.ImageUrl = "MapStream.aspx?ACTION=FULLEXTENT";
}
/// <summary>
/// Add a point feature to point shapefile with an array of values for dbf
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="activeLayer"></param>
/// <param name="fieldValues"></param>
private void AddPoint(Double x,Double y,String activeLayer,String[,] fieldValues)
{
//check: this action is valid only for point shapefiles
pointObj point = pixel2point(new pointObj(x,y,0)); //conver the image point in map point
String shapeFullPath = map.shapepath + "//" + activeLayer + ".shp";
shapefileObj shapefile = new shapefileObj(shapeFullPath,-2);
if(shapefile.type!=(int)mapscript.MS_SHAPEFILE_POINT)
{
//notify action
lblInfo.Text = "This action can be performed only on point shapefiles.";
}
else
{
/*Alternative way to insert a point in the shapefile using shapeObj:
//create line to store the point
lineObj line = new lineObj();
line.add(point);
//create shape
shapeObj shape = new shapeObj((int)MS_SHAPE_TYPE.MS_SHAPE_POINT);
shape.add(line);
//add shape to shapefile
shapefile.add(shape);
*/
shapefile.addPoint(point);
//add record for dbf table
OleDbConnection cn = new OleDbConnection(@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + map.shapepath + ";Extended Properties=dBASE IV;User ID=Admin;Password=");
cn.Open();
OleDbCommand com = cn.CreateCommand();
//get field list and value list to use in the query on dbf
String fieldList = "";
String valueList = "";
for(int i=0; i<(fieldValues.Length/2); i++)
{
fieldList = fieldList + fieldValues[i,0];
valueList = valueList + "'" + fieldValues[i,1] + "'";
if(i<((fieldValues.Length/2)-1))
{
fieldList = fieldList + ",";
valueList = valueList + ",";
}
}
com.CommandText = "INSERT INTO " + activeLayer + " (" + fieldList + ") VALUES(" + valueList + ")";
com.CommandType = CommandType.Text;
com.ExecuteNonQuery();
cn.Close();
//notify action
lblInfo.Text = "Point added (" + (shapefile.numshapes + 1) + " features in shapefile).";
}
shapefile.Dispose();
}
/// <summary>
/// Let's do identify
/// </summary>
/// <param name="x">x image coordinate for the point to identify</param>
/// <param name="y">y image coordinate for the point to identify</param>
/// <param name="activeLayer">layer to identify</param>
private void DoIdentify(Double x,String activeLayer)
{
litIdentifyResult.Text = "";
//identify
layerObj layer = map.getLayerByName(activeLayer);
if(layer!=null)
{
layer.template = "dummy"; //for historical reasons
pointObj point = pixel2point(new pointObj(x,0)); //conver the image point in map point
double tolerance = map.width/100; //we use this tolerance
if(layer.queryByPoint(map,point,mapscript.MS_SINGLE,tolerance)==(int)MS_RETURN_VALUE.MS_SUCCESS)
{
//there is a feature to identify
resultCacheObj result = layer.getResults();
if(result.numresults>0)
{
int shapeInd = result.getResult(0).shapeindex;
//int tileInd = result.getResult(0).tileindex;
layer.open();
shapeObj shape=layer.getFeature(shapeInd,-1);
//iterate fields and getting values
for(int i=0; i<layer.numitems; i++)
{
litIdentifyResult.Text += "<BR><B>" + layer.getItem(i) + "</B>=" + shape.getValue(i);
}
layer.close();
}
}
else
{
//there is nothing to identify
System.Diagnostics.Debug.WriteLine("Nothing to identify.");
}
}
}
/// <summary>
/// Conver pixel point coordinates to map point coordinates
/// </summary>
/// <param name="pointPixel">pixel point (from map Image)</param>
/// <returns></returns>
private pointObj pixel2point(pointObj pointPixel)
{
rectObj extent = map.extent;
double mapWidth = extent.maxx - extent.minx;
double mapHeight = extent.maxy - extent.miny;
double xperc;
double yperc;
xperc = pointPixel.x / map.width;
yperc = (map.height-pointPixel.y) / map.height;
double x=extent.minx + xperc*mapWidth;
double y=extent.miny + yperc*mapHeight;
pointObj pointMap = new pointObj(x,0);
return pointMap;
}
/// <summary>
/// Refresh the map
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void butRefresh_Click(object sender,System.EventArgs e)
{
//iterate layers and check visibility
for(int i=0; i<cblLayers.Items.Count;i++)
{
layerObj layer = map.getLayerByName(cblLayers.Items[i].Text);
if(cblLayers.Items[i].Selected)
{
layer.status=(int)mapscript.MS_ON;
}
else
{
layer.status=(int)mapscript.MS_OFF;
}
}
//send image stream from MapServer to ibMap
ibMap.ImageUrl = "MapStream.aspx?ACTION=REFRESHMAP";
}
/// <summary>
/// Restore the original point shapefile (cleared)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void butClear_Click(object sender,System.EventArgs e)
{
String shapeFullPath = map.shapepath + "//" + ddlLayers.SelectedItem.Text + ".shp";
layerObj layer = map.getLayerByName(ddlLayers.SelectedItem.Text);
if(layer.type!=MS_LAYER_TYPE.MS_LAYER_POINT || layer.connectiontype!=MS_CONNECTION_TYPE.MS_SHAPEFILE)
{
//notify action
lblInfo.Text = "This action can be performed only on point shapefiles.";
}
else
{
//Clear the point shapefile by copying its copy
//Create a DirectoryInfo object representing the specified directory.
DirectoryInfo dir = new DirectoryInfo(map.shapepath);
//Get the FileInfo objects for every file that belongs to shapefile in the directory.
FileInfo[] files = dir.GetFiles(ddlLayers.SelectedItem.Text + "Copy.*");
for(int i=0; i<files.Length; i++)
{
//be sure to put a copy of the point shapefile under shapepath,the copy should be called as NameCopy (ie: for POI of this tutorial,we put a shapefile copy called POICopy)
File.Copy(files[i].FullName,map.shapepath + "//" + ddlLayers.SelectedItem.Text + files[i].Extension,true);
}
//notify action
lblInfo.Text = "Shapefile cleared.";
}
ibMap.ImageUrl = "MapStream.aspx?ACTION=LAYERDELETE";
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.butRefresh.Click += new System.EventHandler(this.butRefresh_Click);
this.ibMap.Click += new System.Web.UI.ImageClickEventHandler(this.ibMap_Click);
this.butFullExtent.Click += new System.EventHandler(this.butFullExtent_Click);
this.butClear.Click += new System.EventHandler(this.butClear_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
}
The c# code for MapStream.aspx page
This is the complete code to add for the MapStream.aspx page:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.IO;
namespace TutorialMapServer
{
/// <summary>
/// MapStream produce an imagestream for the ibMap control at the Default.aspx page
/// </summary>
public class MapStream : System.Web.UI.Page
{
//private variable for this class
mapObj map;
rectObj originalExtent;
/// <summary>
/// Zoom Mode Enumerator
/// </summary>
private enum ZOOMMODE
{
ZoomIn = 0,
ZoomOut = 1
}
/// <summary>
/// Do a Map Action and send an image stream
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Page_Load(object sender,System.EventArgs e)
{
//read map if existing,otherwhise create a new one from map file
map = (mapObj)Session["MAP"];
if(map==null)
{
map = new mapObj(System.Configuration.ConfigurationSettings.AppSettings["mapFilePath"].ToString());
originalExtent = new rectObj(map.extent.minx,map.extent.miny,map.extent.maxx,map.extent.maxy,0);
Session["ORIGINALEXTENT"]=originalExtent;
}
originalExtent = (rectObj)Session["ORIGINALEXTENT"];
//read x,y
Double x=0;
Double y=0;
if(Request.QueryString["X"]!=null && Request.QueryString["Y"]!=null)
{
x = Double.Parse(Request.QueryString["X"].ToString());
y = Double.Parse(Request.QueryString["Y"].ToString());
}
//let's see which action is necessary
String Action = Request.QueryString["ACTION"].ToString().ToUpper();
switch(Action)
{
case "ZOOMIN":
DoZoom(ZOOMMODE.ZoomIn,x,y);
break;
case "ZOOMOUT":
DoZoom(ZOOMMODE.ZoomOut,y);
break;
case "FULLEXTENT":
DoZoomFullExtent();
break;
}
//refresh
RefreshMap();
//store in session
Session["MAP"]=map;
}
/// <summary>
/// Refresh MapServer map and send the image stream to output
/// </summary>
private void RefreshMap()
{
using(imageObj image = map.draw())
{
byte[] img = image.getBytes();
using (MemoryStream ms = new MemoryStream(img))
{
System.Drawing.Image mapimage = System.Drawing.Image.FromStream(ms);
Bitmap bitmap = (Bitmap)mapimage;
bitmap.Save(Response.OutputStream,System.Drawing.Imaging.ImageFormat.Jpeg);
}
}
}
/// <summary>
/// Do a zoom in or zoom out
/// </summary>
/// <param name="zoomMode">zoomin or zoomout</param>
/// <param name="x">x image coordinate</param>
/// <param name="y">y image coordinate</param>
private void DoZoom(ZOOMMODE zoomMode,Double x,Double y)
{
//Do Zoom In
if(zoomMode==ZOOMMODE.ZoomIn)
{
map.zoomPoint(2,new pointObj(x,0),map.width,map.height,map.extent,null);
}
//Do Zoom Out
if(zoomMode==ZOOMMODE.ZoomOut)
{
map.zoomPoint(-2,null);
}
}
/// <summary>
/// Do a Full Extent (return to Origina Extent)
/// </summary>
private void DoZoomFullExtent()
{
map.extent = originalExtent;
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
}
Code flow
The code is extensively commented,so you should have not difficulties to follow it.
the client from the browser request a map. If it is the first map the INITMAP action is performed and the first map from MapServer is generated
the MapServer map is generated from the mapObj from the mapscript library,by accessing to the data (shapefiles) by the Map File
the map is streamed to the image button control in the default.aspx page from the MapStream.aspx page
the mapObj is stored in session for following access from the client
all the GIS actions (Zoom In,Zoom Out…) from the default.aspx page are performed still by the MapStream.aspx page
Share and Enjoy:
Migrating shapefiles to PostGIS (c# mapscript tutorial,part 6)
Posted by Paolo Corti on July 27,2006
What is PostGIS
PostGIS is an extension for Postgresql RDBMS that spatially enables it for storing GIS content.
It could be considered something similiar to Esri ArcSDE or Oracle Spatial.
In fact PostGIS is for Postgresql what is Esri ArcSDE for Oracle,MS sql Server,Informix,DB2.
PostGIS is OGC compliant and is Open Source,released under the GNU General Public License.
For more info about PostGIS you can take a look here.
PostGIS Installation
First,if it is not on your RDBMS Server,you have to download and install Postgresql. Refer to the Postgresql’s Web Site for doing so.
After you have succesfully installed Postgresql,you can add the PostGIS module. For doing so refer to the PostGIS’s Web Site.
Create PostGIS Database
First create the Postgresql database,using Postgresql in the command window or the pgAdmin III sql window,typing:
CREATE DATABASE "TUTORIAL"
WITH OWNER = psqluser
ENCODING = 'UTF8'
TABLESPACE = pg_default;
After creating the database you have to enable it for storing,with PostGIS,the geographic information.
For doing so you have to enable PL/pgsql procedural language extension.
You have to use the createlang command (under Windows use the command prompt,under the Postgresql’s bin folder):
createlang plpgsql -U psqluser -D TUTORIAL
Now you can succesfully load the PostGIS objects and functions by the lwpostgis.sql script.
psql -U psqluser -d TUTORIAL -f lwpostgis.sql
After doing so,you can see that a lot of object and functions are now loaded in TUTORIAL database.
This objects and functions are necessary for PostGIS to work.
Load data in PostGIS database
There are several ways in order to load geometry objects in PostGIS.
It is possible to load data using sql,or using the Loader utility (shp2pgsql command,similiar to the shp2sde command for ArcSDE). In fact the loader utility is a command that will generate the sql with the INSERT statment to load geometries in PostGIS.
Let’s see this two ways.
1. Load data in PostGIS using sql
Create a table named TEST:
BEGIN;
CREATE TABLE "test" ("id" int4,"name" varchar(20));
SELECT AddGeometryColumn('','test','the_geom','-1','LINESTRING',2);
END;
Let’s load the first linestring gis object in this table,using the GeomFromText function:
INSERT INTO "test" ("id","name","the_geom") VALUES (1,'First Geometry',GeomFromText('LINESTRING(1 1,2 2,3 3,3 4)'));
Let’s retrieve this GIS object with the database:
SELECT id,name,the_geom FROM TEST;
> 1;"First Geometry";"01020000..."
As we can notice,the geometry information with a simple SELECT statment is retrieved as is stored in PostGIS database: as a binary string.
If we wan’t to retrieve the geometry information as a simple Open GIS Well Known Text Format string,we can use another function that was created in the TUTORIAL database by the lwpostgis.sql script: AsText
SELECT id,AsText(the_geom) FROM TEST;
> 1;"First Geometry";"LINESTRING(1 1,3 4)"
2. Load data (shapefiles) in PostGIS using the shp2pgsql command
To load shapefiles in PostGIS database you can use the Loader utility: the shp2pgsql command (under bin’s folder). This utility simply creates from any shapefile an sql file that can be used in PostGIS to load the shapefile in the database.
We will now load the tutorial’s shapefiles (compfun.shp,poi.shp,vestizioni.shp,zone.shp) into the TUTORIAL database we created with PostGIS.
First we will use the shp2pgsql command to produce the sql files:
shp2pgsql C:/training/mapServerTutorial/data/poi.shp poi > C:/training/mapServerTutorial/data/poi.sql
shp2pgsql C:/training/mapServerTutorial/data/compfun.shp compfun > C:/training/mapServerTutorial/data/compfun.sql
shp2pgsql C:/training/mapServerTutorial/data/vestizioni.shp vestizioni > C:/training/mapServerTutorial/data/vestizioni.sql
shp2pgsql C:/training/mapServerTutorial/data/zone.shp zone > C:/training/mapServerTutorial/data/zone.sql
Now we can use the 4 sql files produced with PostGIS: these sql files will phisically load the shapefile geometries in 4 PostGIS tables.
Executing the poi.sql script will create in TUTORIAL database the poi table:
BEGIN;
CREATE TABLE "poi" (gid serial PRIMARY KEY,"poi_time" varchar,"poi_user" varchar);
SELECT AddGeometryColumn('','poi','POINT',2);
INSERT INTO "poi" ("poi_time","poi_user",the_geom) VALUES ('04/08/2006,16.23.41','Paolo','0101000000713D0A170A7A3141B81E853B33DB5041');
INSERT INTO "poi" ("poi_time",16.24.09','01010000005C8FC2D5FB79314152B81E9971DB5041');
END;
Executing the compfun.sql script will create in TUTORIAL database the compfun table:
BEGIN;
CREATE TABLE "compfun" (gid serial PRIMARY KEY,"objectid" int8,"codarea" int8,"shape_area" numeric,"shape_len" numeric);
SELECT AddGeometryColumn('','compfun','MULTIPOLYGON',2);
INSERT INTO "compfun" ("objectid","codarea","shape_area","shape_len",the_geom) VALUES ('4052','2801','3.45349032455e+006','8.16732468815e+003','0106000000010000000103000000010000001D00000040996FAA397B3141E07CD98012DC50414016BF4B637B314120EDD71DF2DB5041C0C48863797C3141C026D7070CDC5041C08D9B964B7D3141C040084D8DDB504100750AE32F7B3141102BE15F19DB5041809E6B9AC87A3141906226D602DB504180B38C4A967931410077244BC0DA504100CCFE59B177314160708AEE51DA504180DD9DBA9F76314100C6939B17DA5041C04FAAF651763141706CA3D154DA504140EF3C1B8E7531411049C4A0EFDA5041003A43365D753141004DABBDECDA5041000172392C753141B0E26CBB34DB5041C080CC767F743141F057440D93DB5041402243CBBE74314110CE02459ADB5041003E9495AA743141603B417EA6DB5041C04DC6C64E753141E05D04A8BADB5041402243CBBE7431416060D6F125DC5041C080CC767F743141C0C546F137DC5041809068EA7374314150E422FE4EDC5041805FFBE4EC743141B00EB4615DDC5041403275FC62753141F05CE8A069DC504100C2B632047631414029A95177DC504140BA7EB6B97631419096E78A83DC5041C094C6072A773141C0A93C2E8CDC50410017E6B7897931416003F8D6A6DC504100DC11CE5E7A3141A0551333B0DC5041C0B399BBFB7A3141D0664F850DDC504140996FAA397B3141E07CD98012DC5041');
END;
Executing the vestizioni.sql script will create in TUTORIAL database the vestizioni table:
BEGIN;
CREATE TABLE "vestizioni" (gid serial PRIMARY KEY,"classe" int4,"tipo" int4,"cod" int4,"origine" int4,'vestizioni','MULTILINESTRING',2);
INSERT INTO "vestizioni" ("objectid","classe","tipo","cod","origine",the_geom) VALUES ('89833','8','1','801','2','2.14449030864e+001','010500000001000000010200000002000000403738813D7C3141A08D27B907DC5041800BC76B457C314190779DBD02DC5041');
INSERT INTO "vestizioni" ("objectid",the_geom) VALUES ('89834','5.51685811591e+000','01050000000100000001020000000200000040DFB2743B7C3141B0E4FA0009DC5041403738813D7C3141A08D27B907DC5041');
INSERT INTO "vestizioni" ("objectid",the_geom) VALUES ('89835','9.23513291375e+000','01050000000100000001020000000200000040CB8C02387C3141C0C057250BDC504140DFB2743B7C3141B0E4FA0009DC5041');
/*
other omitted INSERT statments...
*/
END;
Executing the zone.sql script will create in TUTORIAL database the zone table:
BEGIN;
CREATE TABLE "zone" (gid serial PRIMARY KEY,"numlott" int8,"lottoid" numeric,'zone',2);
INSERT INTO "zone" ("objectid","numlott","lottoid",the_geom) VALUES ('46929','7','107','117','2.80111700000e+006','5.68543477776e+003','3.01854931948e+002','0106000000010000000103000000010000000700000040900304607A3141900DF7EA83DB5041C0D4C76B7C7A3141205BC65C72DB5041C0E425CB7C7A314120791E9271DB5041C0E80CE8797A3141201B4EDF70DB5041803FC859407A3141F018096069DB5041C07A13F7207A314160903B487DDB504140900304607A3141900DF7EA83DB5041');
INSERT INTO "zone" ("objectid",the_geom) VALUES ('46935','110','2.80111000000e+006','6.65516688758e+003','3.94268253922e+002','01060000000100000001030000000100000007000000008FFB54307A314140C89894A1DB504180AF5AF5427A314100B92A0E96DB5041408C671FB9793141B0F4188D88DB50410008CE1BAD793141E063DA5990DB5041C03447E0D4793141F070036194DB5041C0C0EC31CF7931411030BA1498DB5041008FFB54307A314140C89894A1DB5041');
INSERT INTO "zone" ("objectid",the_geom) VALUES ('46937','116','2.80111600000e+006','7.15624610182e+003','3.39270025503e+002','01060000000100000001030000000100000005000000C07A13F7207A314160903B487DDB5041803FC859407A3141F018096069DB5041C02DDE73F8793141C0A7E30960DB50410009F5D4D679314140E3658775DB5041C07A13F7207A314160903B487DDB5041');
/*
other omitted INSERT statments...
*/
END;
Connection MapFile layer to PostGIS (c# mapscript tutorial,part 7)
Posted by Paolo Corti on August 1,2006
First I am going to show the difference between connecting the MapFile to a shapefile layer and to a PostGIS layer,then I will show how to adapt your MapFile to work with PostGIS.
Shapefile Connection
Here is a sample connection from MapFile to a shapefile called “compfun”.
First you need to declare the path to the shape data (SHAPEPATH).
Then for each shapefile layer you have to set the shapefile’s data source (DATA) that is the name of the shapefile.
Here the shapefile is named compfun.shp so DATA is set to “compfun”.
Note that NAME can be anything,it means how in your MapFile want to call the shapefile layer.
MAP
...
SHAPEPATH "C:/training/mapServerTutorial/data"
...
LAYER
NAME "compfun"
TYPE POLYGON
STATUS ON
DATA "compfun"
CLASS
STYLE
COLOR 255 235 190
OUTLINECOLOR 0 0 0
SYMBOL 0
END
END
END
...
PostGIS Connection
All the connection properties can be defined in the LAYER section of MapFile.
ObvIoUsly there is no need to set a SHAPEPATH attribute,unless you want to serve some shapefile layer togheter with PostGIS layers.
Basically in the LAYER section you need to set 3 attributes.
CONNECTIONTYPE has to be set to “postgis”
CONNECTION has to be set to the connection string needed for PostGIS
DATA has to be set with a sql select to the geometry column from the layer table
The connection string needs some attribute to be set:
HOST hostname of the Postgresql server
DBNAME name of the database where your PostGIS data are stored
USER name of the user
PASSWORD
PORT you can omit this attribute if you installed Postgresql at the default port (5432)
Also for PostGIS layers,as for shapefile layers,NAME can be anything. It is how you want to call the layer in the MapServer context.
LAYER
CONNECTIONTYPE postgis
CONNECTION "host=localhost dbname=TUTORIAL user=psqluser password=psqluser port=5432"
DATA "the_geom FROM compfun"
NAME "compfun"
TYPE POLYGON
STATUS ON
CLASS
STYLE
COLOR 255 235 190
OUTLINECOLOR 0 0 0
SYMBOL 0
END
END
END
Modify the MapFile tutorial’s file
You can use the csharptutorial_postgis.map MapFile included with the tutorial data.
Or if you prefer you can copy the following text in a new text file named csharptutorial_postgis.map.
MAP
NAME "Zone Samples"
SHAPEPATH "C:/training/mapServerTutorial/data"
SIZE 400 400
STATUS ON
EXTENT 1143759 4417539 1146436 4420390
UNITS METERS
FONTSET "fonts/fonts.list"
WEB
IMAGEPATH "C:/Inetpub/wwwroot/temp"
IMAGEURL "C:/Inetpub/wwwroot/temp"
END
SYMBOL
NAME "circle"
TYPE ellipse
FILLED true
POINTS
1 1
END
END
LAYER
CONNECTIONTYPE postgis
CONNECTION "host=localhost dbname=TUTORIAL user=psqluser password=psqluser port=5432"
DATA "the_geom FROM compfun"
NAME "compfun"
TYPE POLYGON
STATUS ON
CLASS
STYLE
COLOR 255 235 190
OUTLINECOLOR 0 0 0
SYMBOL 0
END
END
END
LAYER
NAME "zone"
CONNECTIONTYPE postgis
CONNECTION "host=localhost dbname=TUTORIAL user=psqluser password=psqluser port=5432"
DATA "the_geom FROM zone"
TYPE POLYGON
STATUS ON
CLASSITEM "COD"
LABELITEM "NUMLOTT"
CLASS
EXPRESSION ([COD]=103)
STYLE
COLOR 230 50 0
OUTLINECOLOR 0 0 0
SYMBOL 0
END
END
CLASS
EXPRESSION ([COD]=105 OR [COD]=106 OR [COD]=107 OR [COD]=108 OR [COD]=102)
STYLE
COLOR 255 235 230
OUTLINECOLOR 0 0 0
SYMBOL 0
END
LABEL
COLOR 0 0 0
FONT verdana
TYPE TRUETYPE
SIZE 7
POSITION CC
END
END
CLASS
EXPRESSION ([COD]=109 OR [COD]=101)
STYLE
COLOR 255 196 171
OUTLINECOLOR 0 0 0
SYMBOL 0
END
END
CLASS
STYLE
COLOR 225 225 225
OUTLINECOLOR 0 0 0
SYMBOL 0
END
END
END
LAYER
CONNECTIONTYPE postgis
CONNECTION "host=localhost dbname=TUTORIAL user=psqluser password=psqluser port=5432"
DATA "the_geom FROM vestizioni"
NAME "vestizioni"
TYPE LINE
STATUS ON
CLASS
STYLE
COLOR 0 0 0
SYMBOL 0
END
END
END
LAYER
CONNECTIONTYPE postgis
CONNECTION "host=localhost dbname=TUTORIAL user=psqluser password=psqluser port=5432"
DATA "the_geom FROM poi"
NAME "POI"
TYPE POINT
STATUS ON
LABELITEM "POI_TIME"
CLASS
SIZE 10
STYLE
COLOR 255 0 0
OUTLINECOLOR 0 0 0
SYMBOL "circle"
END
TEXT ([POI_USER],[POI_TIME])
LABEL
COLOR 255 0 0
FONT verdana
TYPE TRUETYPE
SIZE 7
POSITION LC
WRAP " "
END
END
END
END
In the next step you will adapt the c# to work indifferently with shapefile or PostGIS data.
Adapting C# code to work indifferently with shapefile or PostGIS layers (c# mapscript tutorial,part 8) Posted by Paolo Corti on September 14,2006 In this section we will adapt the ASP .Net Tutorial to work both with shapefiles or with PostGIS layers. The only c# code that needs to be modified is only the code that updates the point layer. The tutorial is composed of basically 2 methods that update a point layer: the first method adds a point and its attributes to the point layer (AddPoint method) the second method delete all the points from the point layer (butClear_Click) After you will terminate this step the tutorial will work both with shapefiles and PostGIS layers. Adding a reference to Postgresql .NET dll to the Visual Studio solution You will connect to PostGresql from your ASP .NET Project. To do so I have chosen to use the Npgsql .NET Data Provider. You should find it with your Postgresql distribution. Basically you need the Npgsql.dll and the Mono.Securirty.dll to be added as .NET references for your Visual Studio Solution. Modifying web.conf application key to use a MapFile with PostGIS connection Update the sections of the web.config file. First you need to replace the value for the key “mapFilePath” with the correct value: the full path to the MapFile that is connecting layers with PostGis. Then you have to add a second key,named “postgresqlConnectionString” with the value setted to the connection string for Postgresql. There parameters for the connection string are similiar to the ones that you used for connecting to Postgresql via the MapFile. <appSettings> <!-- Path to MapFile --> <add key="mapFilePath" value = "C:/training/mapServerTutorial/data/csharptutorial.map" /> <add key="postgresqlConnectionString" value = "Server=127.0.0.1;Port=5432;User Id=psqluser;password=psqluser;Database=TUTORIAL;" /> </appSettings> Modifying the c# method for adding points to the layer Delete from the default.cs class the prevIoUsly typed AddPoint method with these new code: /// <summary> /// Add a point feature to point shapefile with an array of values for dbf,or add a point and attributes to a point PostGIS feature class /// </summary> /// <param name="x">x image coordinate</param> /// <param name="y">y image coordinate</param> /// <param name="activeLayer">name of the active layer</param> /// <param name="fieldValues">array with field names and values</param> private void AddPoint(Double x,] fieldValues) { //check if the active layer is a point layer and if the point layer is from a shapefile or from PostGIS layerObj layer = map.getLayerByName(activeLayer); if(layer.type!=MS_LAYER_TYPE.MS_LAYER_POINT) { //notify action lblInfo.Text = "This action can be performed only on point layers."; return; } //convert the image point in map point pointObj point = pixel2point(new pointObj(x,0)); //generate the sql INSERT statment //get field list and value list to use in the query on dbf String fieldList = ""; String valueList = ""; for(int i=0; i<(fieldValues.Length/2); i++) { fieldList = fieldList + fieldValues[i,0]; valueList = valueList + "'" + fieldValues[i,1] + "'"; if(i<((fieldValues.Length/2)-1)) { fieldList = fieldList + ","; valueList = valueList + ","; } } //add the point to a shapefile if(layer.connectiontype==MS_CONNECTION_TYPE.MS_SHAPEFILE) { String shapeFullPath = map.shapepath + "//" + activeLayer + ".shp"; shapefileObj shapefile = new shapefileObj(shapeFullPath,-2); /*Alternative way to insert a point in the shapefile using shapeObj: //create line to store the point lineObj line = new lineObj(); line.add(point); //create shape shapeObj shape = new shapeObj((int)MS_SHAPE_TYPE.MS_SHAPE_POINT); shape.add(line); //add shape to shapefile shapefile.add(shape); */ shapefile.addPoint(point); //add record for dbf table String sqlInsert = "INSERT INTO " + activeLayer + " (" + fieldList + ") VALUES(" + valueList + ")"; OleDbConnection cn = new OleDbConnection(@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + map.shapepath + ";Extended Properties=dBASE IV;User ID=Admin;Password="); cn.Open(); OleDbCommand com = cn.CreateCommand(); com.CommandText = sqlInsert; com.CommandType = CommandType.Text; com.ExecuteNonQuery(); cn.Close(); shapefile.Dispose(); } //add the point to a PostGIS table if(layer.connectiontype==MS_CONNECTION_TYPE.MS_POSTGIS) { //set CurrentCulture according to Postgresql server CultureInfo newCultureInfo = new CultureInfo("en-US"); newCultureInfo.NumberFormat.NaNSymbol = ""; Thread.CurrentThread.CurrentCulture = newCultureInfo; //connect with Postgresql //the sqlInsert includes also the geometry (with shapefile we need to make 2 different steps) String sqlInsert = "INSERT INTO " + activeLayer + " (" + fieldList + ",the_geom) VALUES(" + valueList + ",GeomFromText('POINT(" + point.x.ToString() + " " + point.y.ToString() + ")',-1))"; //reads connection string for Postgresql String connectionString = System.Configuration.ConfigurationSettings.AppSettings["postgresqlConnectionString"].ToString(); Npgsql.NpgsqlConnection cn = new Npgsql.NpgsqlConnection(connectionString); cn.Open(); Npgsql.NpgsqlCommand com = cn.CreateCommand(); com.CommandText = sqlInsert; com.ExecuteNonQuery(); cn.Close(); } //notify action lblInfo.Text = "Point added to " + activeLayer + " point layer."; } This method is accepting 4 parameters: the x,y image coordinates of the point to be inserted in the layer,the active layer name,and an array with the field names and values. The first thing to be performed is to check if the active layer is a point layer. If not we have to stop the execution. If the active layer is a point layer,then we create the real point from the image coordinates. Then we create a string for the INSERT INTO (field1,field2,…) VALUES (value1,value2,…) statment to be executed in the dbf or Postgresql database. We have two differents way to make this INSERT,depending if the layer is a point shapefile or a point PostGIS layer. Updating the shapefile With a shapefile we need to do 2 consecutive steps (would be better to have a transaction just to be sure that both are performed): First we need to generate a new geometry in the shp file using the shapefileObj MapScript class. To do so we can use the shapefileObj .add(shape) or the .addPoint(point) method Then we need to insert a new record in the dbf table. To do so I use an OleDBCommand Updating the PostGIS layer With a PostGIS layer is much easier and powerfull than with a shapefile. We can just use sql to perform the INSERT of both geometric and textual information. And to do so we just need to perform an unique sql statment. To do so I use a NpgsqlCommand object. Modifying the c# method for clearing all teh points from the layer Delete from the default.cs class the prevIoUsly typed butClear_Click method with these new code: /// <summary> /// Restore the original point shapefile or delete all the records from the PostGIS layer /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void butClear_Click(object sender,System.EventArgs e) { String shapeFullPath = map.shapepath + "//" + ddlLayers.SelectedItem.Text + ".shp"; layerObj layer = map.getLayerByName(ddlLayers.SelectedItem.Text); //action allowed only for point layer if(layer.type!=MS_LAYER_TYPE.MS_LAYER_POINT) { //notify action lblInfo.Text = "This action can be performed only on point layers."; return; } //different delete action if layer is shapefile or PostGIS //shapefile layer if(layer.connectiontype==MS_CONNECTION_TYPE.MS_SHAPEFILE) { //Clear the point shapefile by restoring its copy //Create a DirectoryInfo object representing the specified directory. DirectoryInfo dir = new DirectoryInfo(map.shapepath); //Get the FileInfo objects for every file that belongs to shapefile in the directory. FileInfo[] files = dir.GetFiles(ddlLayers.SelectedItem.Text + "Copy.*"); for(int i=0; i<files.Length; i++) { //be sure to put a copy of the point shapefile under shapepath,we put a shapefile copy called POICopy) File.Copy(files[i].FullName,true); } //notify action lblInfo.Text = "Shapefile cleared."; } //PostGIS layer if(layer.connectiontype==MS_CONNECTION_TYPE.MS_POSTGIS) { String connectionString = "Server=127.0.0.1;Port=5432;User Id=psqluser;password=psqluser;Database=TUTORIAL;"; Npgsql.NpgsqlConnection cn = new Npgsql.NpgsqlConnection(connectionString); cn.Open(); Npgsql.NpgsqlCommand com = cn.CreateCommand(); com.CommandText = "DELETE FROM " + ddlLayers.SelectedItem.Text; com.ExecuteNonQuery(); cn.Close(); //notify action lblInfo.Text = "PostGIS layer cleared."; } ibMap.ImageUrl = "MapStream.aspx?ACTION=LAYERDELETE"; } The first thing to be performed is to check if the active layer is a point layer. If not we have to stop the execution. If the active layer is a point layer,then we are going to clear all the point features in this layer. We have two differents way to make this deletion,depending if the layer is a point shapefile or a point PostGIS layer. Deleting features from the shapefile With a shapefile we will copy the original shapefile with 0 features,replacing the actual shapefile,served by MapServer,with n features. Deleting features from a PostGIS layer With a PostGIS layer is much easier and powerfull than with a shapefile. We can just use sql to perform the DELETE of every feature in the PostGIS layer.