/[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 1693 - (hide annotations)
Mon Sep 1 11:23:26 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: 13656 byte(s)
* Thuban/Model/postgisdb.py
(PostGISTable): Extend doc-string
(PostGISTable._fetch_table_information): Set the column index
correctly, pretending ignored columns don't exist.

* test/test_postgis_db.py (TestPostGISIgnoredColumns): New tests
for postgis tables with data types not yet supported by thuban.

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 bh 1693 """A Table in a PostGIS database
133 bh 1605
134 bh 1693 A PostgreSQL table may contain columns with types not (yet)
135     supported by Thuban. Instances of this class ignore those columns
136     and pretend they don't exist, i.e. they won't show up in the column
137     descriptions returned by Columns() and other methods.
138     """
139    
140 bh 1605 def __init__(self, db, tablename):
141     """Initialize the PostGISTable.
142    
143     The db parameter should be an instance of PostGISConnection and
144     tablename the name of a table in the database represented by db.
145     """
146     self.db = db
147     self.tablename = tablename
148     self.column_map = {}
149     self._fetch_table_information()
150    
151     def _fetch_table_information(self):
152     """Internal: Update information about the table"""
153     self.columns = []
154     cursor = self.db.cursor()
155     cursor.execute("SELECT * FROM %s WHERE 0=1" % self.tablename)
156     description = cursor.description
157    
158     for i in range(len(description)):
159     for pgtyp, tabletyp in type_map:
160     if pgtyp == description[i][1]:
161 bh 1693 col = PostGISColumn(description[i][0], tabletyp,
162     len(self.columns))
163 bh 1605 break
164     else:
165     if description[i][1] == self.db.geometry_type:
166     self.geometry_column = description[i][0]
167     # No matching table type. Ignore the column.
168     # FIXME: We should at least print a warning about
169     # ignored columns
170     continue
171     self.columns.append(col)
172    
173     for col in self.columns:
174     self.column_map[col.name] = col
175     self.column_map[col.index] = col
176    
177     # Build query string for ReadRowAsDict
178     self.query_stmt = ("SELECT %s from %s"
179     % (", ".join([col.name for col in self.columns]),
180     self.tablename))
181    
182 bh 1638 def DBConnection(self):
183     """Return the dbconnection used by the table"""
184     return self.db
185    
186     def TableName(self):
187     """Return the name of the table in the database"""
188     return self.tablename
189    
190 bh 1658 def Title(self):
191     """Return the title of the table.
192    
193     The title is currently fixed and equal to the tablename
194     """
195     return self.tablename
196    
197 bh 1605 def Dependencies(self):
198     """Return an empty tuple because a PostGISTable depends on nothing else
199     """
200     return ()
201    
202     def NumColumns(self):
203     return len(self.columns)
204    
205     def Columns(self):
206     return self.columns
207    
208     def Column(self, col):
209     return self.column_map[col]
210    
211     def HasColumn(self, col):
212     return self.column_map.has_key(col)
213    
214     def NumRows(self):
215     cursor = self.db.cursor()
216     cursor.execute("SELECT count(*) FROM %s" % self.tablename)
217     return cursor.fetchone()[0]
218    
219 bh 1662 def RowIdToOrdinal(self, gid):
220     """Return the row ordinal given its id"""
221 bh 1605 cursor = self.db.cursor()
222 bh 1662 cursor.execute("SELECT count(*) FROM %s WHERE gid < %d;"
223     % (self.tablename, gid))
224     return cursor.fetchone()[0]
225    
226     def RowOrdinalToId(self, num):
227     """Return the rowid for given its ordinal"""
228     cursor = self.db.cursor()
229     cursor.execute("SELECT gid FROM %s LIMIT 1 OFFSET %d;"
230     % (self.tablename, num))
231     return cursor.fetchone()[0]
232    
233     def ReadRowAsDict(self, row, row_is_ordinal = 0):
234     cursor = self.db.cursor()
235     if row_is_ordinal:
236     stmt = self.query_stmt + " LIMIT 1 OFFSET %d" % row
237     else:
238     stmt = self.query_stmt + " WHERE gid = %d" % row
239     cursor.execute(stmt)
240 bh 1605 result = {}
241     for col, value in zip(self.columns, cursor.fetchone()):
242     result[col.name] = value
243     return result
244    
245 bh 1662 def ReadValue(self, row, col, row_is_ordinal = 0):
246 bh 1605 cursor = self.db.cursor()
247 bh 1662 if row_is_ordinal:
248     stmt = ("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
249     (self.column_map[col].name, self.tablename, row))
250     else:
251     stmt = ("SELECT %s FROM %s WHERE gid = %d" %
252     (self.column_map[col].name, self.tablename, row))
253     cursor.execute(stmt)
254 bh 1605 return cursor.fetchone()[0]
255    
256     def ValueRange(self, col):
257     cursor = self.db.cursor()
258     name = self.column_map[col].name
259     cursor.execute("SELECT min(%s), max(%s) FROM %s" %
260     (name, name, self.tablename))
261     return tuple(cursor.fetchone())
262    
263     def UniqueValues(self, col):
264     cursor = self.db.cursor()
265     name = self.column_map[col].name
266     cursor.execute("SELECT %s FROM %s GROUP BY %s" %
267     (name, self.tablename, name))
268     return [row[0] for row in cursor.fetchall()]
269    
270     def SimpleQuery(self, left, comparison, right):
271     if comparison not in ("==", "!=", "<", "<=", ">=", ">"):
272     raise ValueError("Comparison operator %r not allowed" % comparison)
273    
274     if comparison == "==":
275     comparison = "="
276    
277     if isinstance(right, PostGISColumn):
278     right_template = right.name
279     params = ()
280     else:
281     right_template = "%s"
282     params = (right,)
283    
284     query = "SELECT gid FROM %s WHERE %s %s %s ORDER BY gid;" \
285     % (self.tablename, left.name, comparison, right_template)
286    
287     cursor = self.db.cursor()
288     cursor.execute(query, params)
289     result = []
290     while 1:
291     row = cursor.fetchone()
292     if row is None:
293     break
294     result.append(row[0])
295     return result
296    
297    
298     class PostGISShape:
299    
300     def __init__(self, shapeid, data):
301     self.shapeid = shapeid
302     self.data = data
303    
304     def compute_bbox(self):
305     """
306     Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy)
307     """
308     xs = []
309     ys = []
310     for part in self.Points():
311     for x, y in part:
312     xs.append(x)
313     ys.append(y)
314     return (min(xs), min(ys), max(xs), max(ys))
315    
316     def ShapeID(self):
317     return self.shapeid
318    
319     def Points(self):
320     return wellknowntext.parse_wkt_thuban(self.data)
321    
322     def RawData(self):
323     return self.data
324    
325    
326     shapetype_map = {"POLYGON": SHAPETYPE_POLYGON,
327 bh 1656 "MULTIPOLYGON": SHAPETYPE_POLYGON,
328 bh 1605 "MULTILINESTRING": SHAPETYPE_ARC,
329     "POINT": SHAPETYPE_POINT}
330    
331    
332     class PostGISShapeStore(PostGISTable):
333    
334     """Shapestore interface to a table in a PostGIS database"""
335    
336     def Table(self):
337     """Return self since a PostGISShapeStore is its own table."""
338     return self
339    
340     def OrigShapeStore(self):
341     """Return None since the PostGISShapeStore is not derived from another
342     """
343     return None
344    
345     def ShapeType(self):
346     """Return the type of the shapes in the shapestore."""
347     cursor = self.db.cursor()
348     cursor.execute("SELECT type FROM geometry_columns WHERE"
349     " f_table_name=%s", (self.tablename,))
350     result = cursor.fetchone()[0]
351     cursor.close()
352     return shapetype_map[result]
353    
354     def RawShapeFormat(self):
355     """Return the raw data format of the shape data.
356    
357     For the PostGISShapeStore this is RAW_WKT.
358     """
359     return RAW_WKT
360    
361     def NumShapes(self):
362     # The number of shapes is the same as the number of rows,
363     # assuming that the geometry can't be NULL.
364     return self.NumRows()
365    
366     def BoundingBox(self):
367     """Return the bounding box of all shapes in the postgis table"""
368     minx = miny = maxx = maxy = None
369     x=[]
370     y=[]
371     cursor = self.db.cursor()
372     try:
373 bh 1660 # Using the extent function is postgis specific. An OGC
374     # Simple Features compliant solution would be to use a query
375     # like "SELECT AsText(Envelope(the_geom)) FROM mytable;" and
376     # calculate the bounding box by hand from that
377     cursor.execute("SELECT extent(%s) FROM %s;"
378     % (self.geometry_column, self.tablename))
379 bh 1605 result = cursor.fetchone()
380 bh 1660 if result:
381     (minx, miny), (maxx, maxy) \
382     = wellknowntext.parse_wkt_thuban(result[0])[0]
383     return (minx, miny, maxx, maxy)
384 bh 1605 finally:
385     cursor.close()
386    
387     def Shape(self, shapeid):
388     cursor = self.db.cursor()
389     cursor.execute("SELECT AsText(%s) FROM %s WHERE gid=%d"
390     % (self.geometry_column, self.tablename, shapeid))
391     wkt = cursor.fetchone()[0]
392     cursor.close()
393     return PostGISShape(shapeid, wkt)
394    
395 bh 1658 def AllShapes(self):
396     cursor = self.db.cursor()
397     cursor.execute("SELECT gid, AsText(%s) FROM %s ORDER BY gid"
398     % (self.geometry_column, self.tablename))
399     while 1:
400     result = cursor.fetchone()
401     if result is None:
402     return
403     yield PostGISShape(result[0], result[1])
404    
405    
406 bh 1605 def ShapesInRegion(self, bbox):
407     """Generate all shapes overlapping the region given by bbox."""
408     # IMPORTANT:This will work for PostGIS < 0.8
409     left, bottom, right, top = bbox
410     geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))")
411     % (left, bottom, left, top, right, top, right, bottom,
412     left, bottom))
413     cursor = self.db.cursor()
414     cursor.execute("SELECT gid, AsText(%s) FROM %s"
415     " WHERE %s && GeometryFromText('%s', -1) ORDER BY gid"
416     % (self.geometry_column, self.tablename,
417     self.geometry_column, geom))
418     while 1:
419     result = cursor.fetchone()
420     if result is None:
421 bh 1658 return
422 bh 1605 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