/[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 2059 - (hide annotations)
Wed Feb 11 09:05:40 2004 UTC (21 years ago) by bh
Original Path: trunk/thuban/Thuban/Model/postgisdb.py
File MIME type: text/x-python
File size: 19049 byte(s)
* Thuban/Model/postgisdb.py
(PostGISTable._fetch_table_information): Delegate the creation of
column objects to a different method so that we can extend that in
derived classes
(PostGISTable._create_col_from_description): New. Column object
creation part of _fetch_table_information
(PostGISShapeStore._create_col_from_description): New. Extend
inherited method to handle geometry columns
(PostGISShapeStore.__init__): New parameter geometry_column to
specify which geometry column to use.  Optional but mandatory for
tables with more than one geometry column
(PostGISShapeStore._fetch_table_information): Also use the name of
the geometry column when looking for the srid
(PostGISShapeStore.ShapeType): Also use the name of the geometry
column when looking for the shape type

* test/test_save.py (SaveSessionTest.test_save_postgis): Adapt
NonConnectionStore to changes in the PostGISShapeStore

* test/test_postgis_db.py
(TestPostGISSpecialCases.test_shapestore_two_geom_cols): Test
PostGISShapeStore with tables having two geometry columns.

1 bh 2057 # Copyright (C) 2003, 2004 by Intevation GmbH
2 bh 1605 # 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 bh 1946 def quote_identifier(ident):
41     """Return a quoted version of the identifier ident.
42    
43     The return value is a string that can be put directly into an SQL
44 bh 2057 statement. The quoted identifier is surrounded by double quotes and
45     any double quotes already in the input value are converted to two
46     double quotes. Examples:
47 bh 1946
48     >>> quote_identifier("abc\"def")
49     '"abc""def"'
50     >>> quote_identifier("abc def")
51     '"abc def"'
52     """
53     return '"' + '""'.join(ident.split('"')) + '"'
54    
55    
56 bh 1636 class ConnectionError(Exception):
57    
58     """Class for exceptions occurring when establishing a Databse connection"""
59    
60    
61 bh 1605 class PostGISConnection:
62    
63     """Represent a PostGIS database
64    
65     A PostGISConnection instance has the following public attributes:
66    
67     dbname -- The name of the database
68     host, port -- Host and port to connect to
69     user -- The user name to connect as.
70    
71     All of these attributes are strings and may be empty strings to
72     indicate default values.
73     """
74    
75     def __init__(self, dbname, host="", user="", password="", dbtype="",
76     port=""):
77     self.dbname = dbname
78     self.host = host
79     self.port = port
80     self.user = user
81     self.password = password
82     self.dbtype = dbtype
83 bh 1620 self.connect()
84    
85     def connect(self):
86     """Internal: Establish the database connection"""
87 bh 1605 params = []
88     for name in ("host", "port", "dbname", "user", "password"):
89     val = getattr(self, name)
90     if val:
91     params.append("%s=%s" % (name, val))
92 bh 1636 try:
93     self.connection = psycopg.connect(" ".join(params))
94     except psycopg.OperationalError, val:
95     raise ConnectionError(str(val))
96 bh 1605
97     # determine the OID for the geometry type. This is PostGIS
98     # specific.
99     cursor = self.connection.cursor()
100     cursor.execute("SELECT OID, typname FROM pg_type WHERE"
101     +" typname = 'geometry'")
102     row = cursor.fetchone()
103     self.connection.commit()
104     if row is not None:
105     self.geometry_type = row[0]
106     else:
107     raise ValueError("Can't determine postgres type of geometries")
108    
109 bh 1620 def BriefDescription(self):
110     """Return a brief, one-line description of the connection
111    
112     The return value is suitable for a list box of all database
113     connections.
114     """
115     return ("postgis://%(user)s@%(host)s:%(port)s/%(dbname)s"
116     % self.__dict__)
117    
118 bh 1955 def MatchesParameters(self, parameters):
119     """Return whether the connection matches the dictionary of parameters
120    
121     Return whether instatiating the connection with the given
122     parameters would establish essentially the same connection as
123     self. The connection is essentially the same if the same
124     database (identified by host, port and databasename) is accessed
125     as the same user.
126     """
127     return (parameters["host"] == self.host
128     and parameters["port"] == self.port
129     and parameters["dbname"] == self.dbname
130     and parameters["user"] == self.user)
131    
132 bh 1605 def Close(self):
133     """Close the database connection"""
134     self.connection.close()
135    
136     def GeometryTables(self):
137     """Return a list with the names of all tables with a geometry column"""
138     cursor = self.connection.cursor()
139     cursor.execute("SELECT f_table_name FROM geometry_columns;")
140     result = [row[0] for row in cursor.fetchall()]
141     self.connection.commit()
142     return result
143    
144     def cursor(self):
145     """Return a DB API 2.0 cursor for the database"""
146     return self.connection.cursor()
147    
148    
149    
150     class PostGISColumn:
151    
152 bh 1946 """Column description for a PostGISTable
153 bh 1605
154 bh 1946 In addition to the normal column object attributes name, type and
155     index, PostGISColumn objects have a quoted_name attribute which
156     contains a quoted version of name for use in SQL statements. The
157     quoted_name attribute is mainly intended for internal use by the
158     PostGISTable class.
159     """
160    
161 bh 1605 def __init__(self, name, type, index):
162     self.name = name
163 bh 1946 self.quoted_name = quote_identifier(name)
164 bh 1605 self.type = type
165     self.index = index
166    
167    
168     class PostGISTable:
169    
170 bh 1693 """A Table in a PostGIS database
171 bh 1605
172 bh 1693 A PostgreSQL table may contain columns with types not (yet)
173     supported by Thuban. Instances of this class ignore those columns
174     and pretend they don't exist, i.e. they won't show up in the column
175     descriptions returned by Columns() and other methods.
176     """
177    
178 bh 1605 def __init__(self, db, tablename):
179     """Initialize the PostGISTable.
180    
181     The db parameter should be an instance of PostGISConnection and
182     tablename the name of a table in the database represented by db.
183     """
184     self.db = db
185     self.tablename = tablename
186 bh 1946 # Tablename quoted for use in SQL statements.
187     self.quoted_tablename = quote_identifier(tablename)
188    
189     # Map column names and indices to column objects.
190 bh 1605 self.column_map = {}
191 bh 1946
192 bh 1605 self._fetch_table_information()
193    
194     def _fetch_table_information(self):
195     """Internal: Update information about the table"""
196     self.columns = []
197     cursor = self.db.cursor()
198 bh 1946 cursor.execute("SELECT * FROM %s WHERE 0=1" % self.quoted_tablename)
199 bh 1605 description = cursor.description
200    
201     for i in range(len(description)):
202 bh 2059 col = self._create_col_from_description(i, description[i])
203     if col is not None:
204     self.columns.append(col)
205 bh 1605
206     for col in self.columns:
207     self.column_map[col.name] = col
208     self.column_map[col.index] = col
209    
210     # Build query string for ReadRowAsDict
211     self.query_stmt = ("SELECT %s from %s"
212 bh 1946 % (", ".join([col.quoted_name
213     for col in self.columns]),
214     self.quoted_tablename))
215 bh 1605
216 bh 2059 def _create_col_from_description(self, index, description):
217     """Return the column object for the column described by description
218    
219     The parameter index is the index of the column. The description
220     is a sequence taken from the cursor's description attribute for
221     the column. That means description[0] is the name of the column
222     and description[1] the type.
223    
224     Return None if the column can't be represented for some reason,
225     e.g. because its type is not yet supported or needs to be
226     treated in some special way. Derived classes may extend this
227     method.
228     """
229     for pgtyp, tabletyp in type_map:
230     if pgtyp == description[1]:
231     return PostGISColumn(description[0], tabletyp,
232     len(self.columns))
233     return None
234    
235 bh 1638 def DBConnection(self):
236     """Return the dbconnection used by the table"""
237     return self.db
238    
239     def TableName(self):
240     """Return the name of the table in the database"""
241     return self.tablename
242    
243 bh 1658 def Title(self):
244     """Return the title of the table.
245    
246     The title is currently fixed and equal to the tablename
247     """
248     return self.tablename
249    
250 bh 1605 def Dependencies(self):
251     """Return an empty tuple because a PostGISTable depends on nothing else
252     """
253     return ()
254    
255     def NumColumns(self):
256     return len(self.columns)
257    
258     def Columns(self):
259     return self.columns
260    
261     def Column(self, col):
262     return self.column_map[col]
263    
264     def HasColumn(self, col):
265     return self.column_map.has_key(col)
266    
267     def NumRows(self):
268     cursor = self.db.cursor()
269 bh 1946 cursor.execute("SELECT count(*) FROM %s" % self.quoted_tablename)
270 bh 1605 return cursor.fetchone()[0]
271    
272 bh 1662 def RowIdToOrdinal(self, gid):
273     """Return the row ordinal given its id"""
274 bh 1605 cursor = self.db.cursor()
275 bh 1662 cursor.execute("SELECT count(*) FROM %s WHERE gid < %d;"
276 bh 1946 % (self.quoted_tablename, gid))
277 bh 1662 return cursor.fetchone()[0]
278    
279     def RowOrdinalToId(self, num):
280     """Return the rowid for given its ordinal"""
281     cursor = self.db.cursor()
282     cursor.execute("SELECT gid FROM %s LIMIT 1 OFFSET %d;"
283 bh 1946 % (self.quoted_tablename, num))
284 bh 1662 return cursor.fetchone()[0]
285    
286     def ReadRowAsDict(self, row, row_is_ordinal = 0):
287     cursor = self.db.cursor()
288     if row_is_ordinal:
289     stmt = self.query_stmt + " LIMIT 1 OFFSET %d" % row
290     else:
291     stmt = self.query_stmt + " WHERE gid = %d" % row
292     cursor.execute(stmt)
293 bh 1605 result = {}
294     for col, value in zip(self.columns, cursor.fetchone()):
295     result[col.name] = value
296     return result
297    
298 bh 1662 def ReadValue(self, row, col, row_is_ordinal = 0):
299 bh 1605 cursor = self.db.cursor()
300 bh 1662 if row_is_ordinal:
301     stmt = ("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
302 bh 1946 (self.column_map[col].quoted_name, self.quoted_tablename,
303     row))
304 bh 1662 else:
305     stmt = ("SELECT %s FROM %s WHERE gid = %d" %
306 bh 1946 (self.column_map[col].quoted_name, self.quoted_tablename,
307     row))
308 bh 1662 cursor.execute(stmt)
309 bh 1605 return cursor.fetchone()[0]
310    
311     def ValueRange(self, col):
312     cursor = self.db.cursor()
313 bh 1946 name = self.column_map[col].quoted_name
314 bh 1605 cursor.execute("SELECT min(%s), max(%s) FROM %s" %
315 bh 1946 (name, name, self.quoted_tablename))
316 bh 1605 return tuple(cursor.fetchone())
317    
318     def UniqueValues(self, col):
319     cursor = self.db.cursor()
320 bh 1946 name = self.column_map[col].quoted_name
321 bh 1605 cursor.execute("SELECT %s FROM %s GROUP BY %s" %
322 bh 1946 (name, self.quoted_tablename, name))
323 bh 1605 return [row[0] for row in cursor.fetchall()]
324    
325     def SimpleQuery(self, left, comparison, right):
326     if comparison not in ("==", "!=", "<", "<=", ">=", ">"):
327     raise ValueError("Comparison operator %r not allowed" % comparison)
328    
329     if comparison == "==":
330     comparison = "="
331    
332     if isinstance(right, PostGISColumn):
333 bh 1946 right_template = right.quoted_name
334 bh 1605 params = ()
335     else:
336     right_template = "%s"
337     params = (right,)
338    
339     query = "SELECT gid FROM %s WHERE %s %s %s ORDER BY gid;" \
340 bh 1946 % (self.quoted_tablename, left.quoted_name, comparison,
341     right_template)
342 bh 1605
343     cursor = self.db.cursor()
344     cursor.execute(query, params)
345     result = []
346     while 1:
347     row = cursor.fetchone()
348     if row is None:
349     break
350     result.append(row[0])
351     return result
352    
353    
354     class PostGISShape:
355    
356     def __init__(self, shapeid, data):
357     self.shapeid = shapeid
358     self.data = data
359    
360     def compute_bbox(self):
361     """
362     Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy)
363     """
364     xs = []
365     ys = []
366     for part in self.Points():
367     for x, y in part:
368     xs.append(x)
369     ys.append(y)
370     return (min(xs), min(ys), max(xs), max(ys))
371    
372     def ShapeID(self):
373     return self.shapeid
374    
375     def Points(self):
376     return wellknowntext.parse_wkt_thuban(self.data)
377    
378     def RawData(self):
379     return self.data
380    
381    
382     shapetype_map = {"POLYGON": SHAPETYPE_POLYGON,
383 bh 1656 "MULTIPOLYGON": SHAPETYPE_POLYGON,
384 bh 1605 "MULTILINESTRING": SHAPETYPE_ARC,
385     "POINT": SHAPETYPE_POINT}
386    
387    
388     class PostGISShapeStore(PostGISTable):
389    
390     """Shapestore interface to a table in a PostGIS database"""
391    
392 bh 2059 def __init__(self, db, tablename, geometry_column = None):
393     """Initialize the PostGISShapeStore.
394    
395     The db parameter should be an instance of PostGISConnection and
396     tablename the name of a table in the database represented by db.
397    
398     The geometry_column paramter, if given, should be the name of
399     the geometry column to use. If the name given is not a geometry
400     column, raise a ValueError.
401    
402     If no geometry_column is given, the table must have exactly one
403     geometry column. If it has more than one and the
404     geometry_column is not given, a ValueError will be raised.
405     """
406     self.geometry_column = geometry_column
407     self.geometry_column_was_given = geometry_column is not None
408     PostGISTable.__init__(self, db, tablename)
409    
410     # For convenience, we have a quoted version of the geometry
411     # column in self.quoted_geo_col
412     self.quoted_geo_col = quote_identifier(self.geometry_column)
413    
414 bh 2057 def _fetch_table_information(self):
415     """Extend inherited method to retrieve the SRID"""
416     PostGISTable._fetch_table_information(self)
417     cursor = self.db.cursor()
418     cursor.execute("SELECT srid FROM geometry_columns"
419 bh 2059 " WHERE f_table_name = %s AND f_geometry_column=%s",
420     (self.tablename, self.geometry_column))
421 bh 2057 self.srid = cursor.fetchone()[0]
422    
423 bh 2059 def _create_col_from_description(self, index, description):
424     """Extend the inherited method to find geometry columns
425    
426     If the column indicated by the paramters is a geometry column,
427     record its name in self.geometry_column and a quoted version in
428     self.quoted_geo_col. In any case return the return value of the
429     inherited method.
430     """
431     col = PostGISTable._create_col_from_description(self, index,
432     description)
433     col_name, col_type = description[:2]
434     if self.geometry_column_was_given:
435     if (col_name == self.geometry_column
436     and col_type != self.db.geometry_type):
437     raise TypeError("Column %s in %s is not a geometry column"
438     % (self.geometry_column, self.tablename))
439     else:
440     if col is None:
441     if description[1] == self.db.geometry_type:
442     # The column is a geometry column. If the name of
443     # the geometry column was not given to the
444     # constructor, and we encounter two geometry
445     # columns, raise a value error
446     if self.geometry_column is None:
447     self.geometry_column = description[0]
448     else:
449     raise TypeError("Table %s has two geometry columns"
450     " and no column name was given"
451     % (self.tablename,))
452     return col
453    
454 bh 1605 def Table(self):
455     """Return self since a PostGISShapeStore is its own table."""
456     return self
457    
458     def OrigShapeStore(self):
459     """Return None since the PostGISShapeStore is not derived from another
460     """
461     return None
462    
463     def ShapeType(self):
464     """Return the type of the shapes in the shapestore."""
465     cursor = self.db.cursor()
466     cursor.execute("SELECT type FROM geometry_columns WHERE"
467 bh 2059 " f_table_name=%s AND f_geometry_column=%s",
468     (self.tablename, self.geometry_column))
469 bh 1605 result = cursor.fetchone()[0]
470     cursor.close()
471     return shapetype_map[result]
472    
473     def RawShapeFormat(self):
474     """Return the raw data format of the shape data.
475    
476     For the PostGISShapeStore this is RAW_WKT.
477     """
478     return RAW_WKT
479    
480     def NumShapes(self):
481     # The number of shapes is the same as the number of rows,
482     # assuming that the geometry can't be NULL.
483     return self.NumRows()
484    
485     def BoundingBox(self):
486     """Return the bounding box of all shapes in the postgis table"""
487     minx = miny = maxx = maxy = None
488     x=[]
489     y=[]
490     cursor = self.db.cursor()
491     try:
492 bh 1660 # Using the extent function is postgis specific. An OGC
493     # Simple Features compliant solution would be to use a query
494     # like "SELECT AsText(Envelope(the_geom)) FROM mytable;" and
495     # calculate the bounding box by hand from that
496     cursor.execute("SELECT extent(%s) FROM %s;"
497 bh 1946 % (self.quoted_geo_col, self.quoted_tablename))
498 bh 1605 result = cursor.fetchone()
499 bh 2057 if result[0]:
500 bh 1660 (minx, miny), (maxx, maxy) \
501     = wellknowntext.parse_wkt_thuban(result[0])[0]
502     return (minx, miny, maxx, maxy)
503 bh 1605 finally:
504     cursor.close()
505    
506     def Shape(self, shapeid):
507     cursor = self.db.cursor()
508     cursor.execute("SELECT AsText(%s) FROM %s WHERE gid=%d"
509 bh 1946 % (self.quoted_geo_col, self.quoted_tablename, shapeid))
510 bh 1605 wkt = cursor.fetchone()[0]
511     cursor.close()
512     return PostGISShape(shapeid, wkt)
513    
514 bh 1658 def AllShapes(self):
515     cursor = self.db.cursor()
516     cursor.execute("SELECT gid, AsText(%s) FROM %s ORDER BY gid"
517 bh 1946 % (self.quoted_geo_col, self.quoted_tablename))
518 bh 1658 while 1:
519     result = cursor.fetchone()
520     if result is None:
521     return
522     yield PostGISShape(result[0], result[1])
523    
524    
525 bh 1605 def ShapesInRegion(self, bbox):
526     """Generate all shapes overlapping the region given by bbox."""
527     # IMPORTANT:This will work for PostGIS < 0.8
528     left, bottom, right, top = bbox
529     geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))")
530     % (left, bottom, left, top, right, top, right, bottom,
531     left, bottom))
532     cursor = self.db.cursor()
533     cursor.execute("SELECT gid, AsText(%s) FROM %s"
534 bh 2057 " WHERE %s && GeometryFromText('%s', %d) ORDER BY gid"
535 bh 1946 % (self.quoted_geo_col, self.quoted_tablename,
536 bh 2057 self.quoted_geo_col, geom, self.srid))
537 bh 1605 while 1:
538     result = cursor.fetchone()
539     if result is None:
540 bh 1658 return
541 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