/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/Model/postgisdb.py
ViewVC logotype

Annotation of /branches/WIP-pyshapelib-bramz/Thuban/Model/postgisdb.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1638 - (hide annotations)
Fri Aug 22 18:19:14 2003 UTC (21 years, 6 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/postgisdb.py
File MIME type: text/x-python
File size: 12261 byte(s)
Implement saving a session with a postgis connection

* Resources/XML/thuban-0.9.dtd (dbconnection, dbshapesource) New
elements for database connections and shapestores using db
connections
(session): Add the dbconnections to the content model

* Thuban/Model/save.py (SessionSaver.write_db_connections): New.
Write the db connections
(SessionSaver.write_session): Call write_db_connections to write
the connection before the data sources
(SessionSaver.write_data_containers): Handle postgis shapestores

* test/test_save.py (SaveSessionTest.thubanids)
(SaveSessionTest.thubanidrefs): Update for new DTD
(SaveSessionTest.test_save_postgis): New. Test saving a session
with postgis connections

* Thuban/Model/postgisdb.py (PostGISTable.DBConnection)
(PostGISTable.TableName): New accessor methods for the connection
and table name

* test/test_postgis_db.py (TestPostGISTable.test_dbconn)
(TestPostGISTable.test_dbname): New methods to test the new
PostGISConnection methods

1 bh 1605 # Copyright (C) 2003 by Intevation GmbH
2     # Authors:
3     # Martin Mueller <[email protected]>
4     # Bernhard Herzog <[email protected]>
5     #
6     # This program is free software under the GPL (>=v2)
7     # Read the file COPYING coming with the software for details.
8    
9     """Basic interface to a PostGIS database"""
10    
11     from __future__ import generators
12    
13     try:
14     import psycopg
15     except ImportError:
16     psycopg = None
17    
18     import table
19     import wellknowntext
20    
21     from data import SHAPETYPE_POLYGON, SHAPETYPE_ARC, SHAPETYPE_POINT, RAW_WKT
22    
23 bh 1625 def has_postgis_support():
24     """Return whether this Thuban instance supports PostGIS connections
25 bh 1605
26 bh 1625 Having PostGIS support means that the psycopg module can be
27     imported.
28     """
29     return psycopg is not None
30    
31 bh 1631 def psycopg_version():
32     return psycopg.__version__
33    
34 bh 1605 if psycopg is not None:
35     type_map = [(psycopg.STRING, table.FIELDTYPE_STRING),
36     (psycopg.INTEGER, table.FIELDTYPE_INT),
37     (psycopg.FLOAT, table.FIELDTYPE_DOUBLE)]
38    
39 bh 1636
40     class ConnectionError(Exception):
41    
42     """Class for exceptions occurring when establishing a Databse connection"""
43    
44    
45 bh 1605 class PostGISConnection:
46    
47     """Represent a PostGIS database
48    
49     A PostGISConnection instance has the following public attributes:
50    
51     dbname -- The name of the database
52     host, port -- Host and port to connect to
53     user -- The user name to connect as.
54    
55     All of these attributes are strings and may be empty strings to
56     indicate default values.
57     """
58    
59     def __init__(self, dbname, host="", user="", password="", dbtype="",
60     port=""):
61     self.dbname = dbname
62     self.host = host
63     self.port = port
64     self.user = user
65     self.password = password
66     self.dbtype = dbtype
67 bh 1620 self.connect()
68    
69     def connect(self):
70     """Internal: Establish the database connection"""
71 bh 1605 params = []
72     for name in ("host", "port", "dbname", "user", "password"):
73     val = getattr(self, name)
74     if val:
75     params.append("%s=%s" % (name, val))
76 bh 1636 try:
77     self.connection = psycopg.connect(" ".join(params))
78     except psycopg.OperationalError, val:
79     raise ConnectionError(str(val))
80 bh 1605
81     # determine the OID for the geometry type. This is PostGIS
82     # specific.
83     cursor = self.connection.cursor()
84     cursor.execute("SELECT OID, typname FROM pg_type WHERE"
85     +" typname = 'geometry'")
86     row = cursor.fetchone()
87     self.connection.commit()
88     if row is not None:
89     self.geometry_type = row[0]
90     else:
91     raise ValueError("Can't determine postgres type of geometries")
92    
93 bh 1620 def BriefDescription(self):
94     """Return a brief, one-line description of the connection
95    
96     The return value is suitable for a list box of all database
97     connections.
98     """
99     return ("postgis://%(user)s@%(host)s:%(port)s/%(dbname)s"
100     % self.__dict__)
101    
102 bh 1605 def Close(self):
103     """Close the database connection"""
104     self.connection.close()
105    
106     def GeometryTables(self):
107     """Return a list with the names of all tables with a geometry column"""
108     cursor = self.connection.cursor()
109     cursor.execute("SELECT f_table_name FROM geometry_columns;")
110     result = [row[0] for row in cursor.fetchall()]
111     self.connection.commit()
112     return result
113    
114     def cursor(self):
115     """Return a DB API 2.0 cursor for the database"""
116     return self.connection.cursor()
117    
118    
119    
120     class PostGISColumn:
121    
122     """Column description for a PostGISTable"""
123    
124     def __init__(self, name, type, index):
125     self.name = name
126     self.type = type
127     self.index = index
128    
129    
130     class PostGISTable:
131    
132     """A Table in a PostGIS database"""
133    
134     def __init__(self, db, tablename):
135     """Initialize the PostGISTable.
136    
137     The db parameter should be an instance of PostGISConnection and
138     tablename the name of a table in the database represented by db.
139     """
140     self.db = db
141     self.tablename = tablename
142     self.column_map = {}
143     self._fetch_table_information()
144    
145     def _fetch_table_information(self):
146     """Internal: Update information about the table"""
147     self.columns = []
148     cursor = self.db.cursor()
149     cursor.execute("SELECT * FROM %s WHERE 0=1" % self.tablename)
150     description = cursor.description
151    
152     for i in range(len(description)):
153     for pgtyp, tabletyp in type_map:
154     if pgtyp == description[i][1]:
155     col = PostGISColumn(description[i][0], tabletyp, i)
156     break
157     else:
158     if description[i][1] == self.db.geometry_type:
159     self.geometry_column = description[i][0]
160     # No matching table type. Ignore the column.
161     # FIXME: We should at least print a warning about
162     # ignored columns
163     continue
164     self.columns.append(col)
165    
166     for col in self.columns:
167     self.column_map[col.name] = col
168     self.column_map[col.index] = col
169    
170     # Build query string for ReadRowAsDict
171     self.query_stmt = ("SELECT %s from %s"
172     % (", ".join([col.name for col in self.columns]),
173     self.tablename))
174    
175 bh 1638 def DBConnection(self):
176     """Return the dbconnection used by the table"""
177     return self.db
178    
179     def TableName(self):
180     """Return the name of the table in the database"""
181     return self.tablename
182    
183 bh 1605 def Dependencies(self):
184     """Return an empty tuple because a PostGISTable depends on nothing else
185     """
186     return ()
187    
188     def NumColumns(self):
189     return len(self.columns)
190    
191     def Columns(self):
192     return self.columns
193    
194     def Column(self, col):
195     return self.column_map[col]
196    
197     def HasColumn(self, col):
198     return self.column_map.has_key(col)
199    
200     def NumRows(self):
201     cursor = self.db.cursor()
202     cursor.execute("SELECT count(*) FROM %s" % self.tablename)
203     return cursor.fetchone()[0]
204    
205     def ReadRowAsDict(self, row):
206     cursor = self.db.cursor()
207     cursor.execute(self.query_stmt + " LIMIT 1 OFFSET %d" % row)
208     result = {}
209     for col, value in zip(self.columns, cursor.fetchone()):
210     result[col.name] = value
211     return result
212    
213     def ReadValue(self, row, col):
214     cursor = self.db.cursor()
215     cursor.execute("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
216     (self.column_map[col].name, self.tablename, row))
217     return cursor.fetchone()[0]
218    
219     def ValueRange(self, col):
220     cursor = self.db.cursor()
221     name = self.column_map[col].name
222     cursor.execute("SELECT min(%s), max(%s) FROM %s" %
223     (name, name, self.tablename))
224     return tuple(cursor.fetchone())
225    
226     def UniqueValues(self, col):
227     cursor = self.db.cursor()
228     name = self.column_map[col].name
229     cursor.execute("SELECT %s FROM %s GROUP BY %s" %
230     (name, self.tablename, name))
231     return [row[0] for row in cursor.fetchall()]
232    
233     def SimpleQuery(self, left, comparison, right):
234     if comparison not in ("==", "!=", "<", "<=", ">=", ">"):
235     raise ValueError("Comparison operator %r not allowed" % comparison)
236    
237     if comparison == "==":
238     comparison = "="
239    
240     if isinstance(right, PostGISColumn):
241     right_template = right.name
242     params = ()
243     else:
244     right_template = "%s"
245     params = (right,)
246    
247     query = "SELECT gid FROM %s WHERE %s %s %s ORDER BY gid;" \
248     % (self.tablename, left.name, comparison, right_template)
249    
250     cursor = self.db.cursor()
251     cursor.execute(query, params)
252     result = []
253     while 1:
254     row = cursor.fetchone()
255     if row is None:
256     break
257     result.append(row[0])
258     return result
259    
260    
261     class PostGISShape:
262    
263     def __init__(self, shapeid, data):
264     self.shapeid = shapeid
265     self.data = data
266    
267     def compute_bbox(self):
268     """
269     Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy)
270     """
271     xs = []
272     ys = []
273     for part in self.Points():
274     for x, y in part:
275     xs.append(x)
276     ys.append(y)
277     return (min(xs), min(ys), max(xs), max(ys))
278    
279     def ShapeID(self):
280     return self.shapeid
281    
282     def Points(self):
283     return wellknowntext.parse_wkt_thuban(self.data)
284    
285     def RawData(self):
286     return self.data
287    
288    
289     shapetype_map = {"POLYGON": SHAPETYPE_POLYGON,
290     "MULTILINESTRING": SHAPETYPE_ARC,
291     "POINT": SHAPETYPE_POINT}
292    
293    
294     class PostGISShapeStore(PostGISTable):
295    
296     """Shapestore interface to a table in a PostGIS database"""
297    
298     def Table(self):
299     """Return self since a PostGISShapeStore is its own table."""
300     return self
301    
302     def OrigShapeStore(self):
303     """Return None since the PostGISShapeStore is not derived from another
304     """
305     return None
306    
307     def ShapeType(self):
308     """Return the type of the shapes in the shapestore."""
309     cursor = self.db.cursor()
310     cursor.execute("SELECT type FROM geometry_columns WHERE"
311     " f_table_name=%s", (self.tablename,))
312     result = cursor.fetchone()[0]
313     cursor.close()
314     return shapetype_map[result]
315    
316     def RawShapeFormat(self):
317     """Return the raw data format of the shape data.
318    
319     For the PostGISShapeStore this is RAW_WKT.
320     """
321     return RAW_WKT
322    
323     def NumShapes(self):
324     # The number of shapes is the same as the number of rows,
325     # assuming that the geometry can't be NULL.
326     return self.NumRows()
327    
328     def BoundingBox(self):
329     """Return the bounding box of all shapes in the postgis table"""
330     minx = miny = maxx = maxy = None
331     x=[]
332     y=[]
333     cursor = self.db.cursor()
334     try:
335     stmt = ("SELECT AsText(Envelope(%s)) FROM %s;"
336     % (self.geometry_column, self.tablename))
337     cursor.execute(stmt)
338     result = cursor.fetchone()
339     while result:
340     result = result[0]
341     # Here we must do some parsing through the result string
342     # to get the points out of the polygon which representes
343     # the bounding box The first and the last point of a
344     # polygon are identical
345     result = result.split("(")[2]
346     result = result.split(")")[0]
347     points = result.split(",")
348     del points[4] # Remove the last point
349     for point in points:
350     px, py = point.split()
351     x.append(float(px))
352     y.append(float(py))
353     result = cursor.fetchone()
354     finally:
355     cursor.close()
356     if not x:
357     # Empty table
358     return None
359     return (min(x), min(y), max(x), max(y))
360    
361     def Shape(self, shapeid):
362     cursor = self.db.cursor()
363     cursor.execute("SELECT AsText(%s) FROM %s WHERE gid=%d"
364     % (self.geometry_column, self.tablename, shapeid))
365     wkt = cursor.fetchone()[0]
366     cursor.close()
367     return PostGISShape(shapeid, wkt)
368    
369     def ShapesInRegion(self, bbox):
370     """Generate all shapes overlapping the region given by bbox."""
371     # IMPORTANT:This will work for PostGIS < 0.8
372     left, bottom, right, top = bbox
373     geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))")
374     % (left, bottom, left, top, right, top, right, bottom,
375     left, bottom))
376     cursor = self.db.cursor()
377     cursor.execute("SELECT gid, AsText(%s) FROM %s"
378     " WHERE %s && GeometryFromText('%s', -1) ORDER BY gid"
379     % (self.geometry_column, self.tablename,
380     self.geometry_column, geom))
381     while 1:
382     result = cursor.fetchone()
383     if result is None:
384     raise StopIteration
385     yield PostGISShape(result[0], result[1])

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26