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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1636 - (show annotations)
Fri Aug 22 17:20:29 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: 12038 byte(s)
* Thuban/Model/postgisdb.py (ConnectionError): New exception class
for exceptions occurring when establishing a Database connection
(PostGISConnection.connect): Catch psycopg.OperationalError during
connects and raise ConnectionError.

* test/test_postgis_db.py (TestPostgisDBExceptions): New class for
tests for database exceptions

1 # 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 def has_postgis_support():
24 """Return whether this Thuban instance supports PostGIS connections
25
26 Having PostGIS support means that the psycopg module can be
27 imported.
28 """
29 return psycopg is not None
30
31 def psycopg_version():
32 return psycopg.__version__
33
34 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
40 class ConnectionError(Exception):
41
42 """Class for exceptions occurring when establishing a Databse connection"""
43
44
45 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 self.connect()
68
69 def connect(self):
70 """Internal: Establish the database connection"""
71 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 try:
77 self.connection = psycopg.connect(" ".join(params))
78 except psycopg.OperationalError, val:
79 raise ConnectionError(str(val))
80
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 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 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 def Dependencies(self):
176 """Return an empty tuple because a PostGISTable depends on nothing else
177 """
178 return ()
179
180 def NumColumns(self):
181 return len(self.columns)
182
183 def Columns(self):
184 return self.columns
185
186 def Column(self, col):
187 return self.column_map[col]
188
189 def HasColumn(self, col):
190 return self.column_map.has_key(col)
191
192 def NumRows(self):
193 cursor = self.db.cursor()
194 cursor.execute("SELECT count(*) FROM %s" % self.tablename)
195 return cursor.fetchone()[0]
196
197 def ReadRowAsDict(self, row):
198 cursor = self.db.cursor()
199 cursor.execute(self.query_stmt + " LIMIT 1 OFFSET %d" % row)
200 result = {}
201 for col, value in zip(self.columns, cursor.fetchone()):
202 result[col.name] = value
203 return result
204
205 def ReadValue(self, row, col):
206 cursor = self.db.cursor()
207 cursor.execute("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
208 (self.column_map[col].name, self.tablename, row))
209 return cursor.fetchone()[0]
210
211 def ValueRange(self, col):
212 cursor = self.db.cursor()
213 name = self.column_map[col].name
214 cursor.execute("SELECT min(%s), max(%s) FROM %s" %
215 (name, name, self.tablename))
216 return tuple(cursor.fetchone())
217
218 def UniqueValues(self, col):
219 cursor = self.db.cursor()
220 name = self.column_map[col].name
221 cursor.execute("SELECT %s FROM %s GROUP BY %s" %
222 (name, self.tablename, name))
223 return [row[0] for row in cursor.fetchall()]
224
225 def SimpleQuery(self, left, comparison, right):
226 if comparison not in ("==", "!=", "<", "<=", ">=", ">"):
227 raise ValueError("Comparison operator %r not allowed" % comparison)
228
229 if comparison == "==":
230 comparison = "="
231
232 if isinstance(right, PostGISColumn):
233 right_template = right.name
234 params = ()
235 else:
236 right_template = "%s"
237 params = (right,)
238
239 query = "SELECT gid FROM %s WHERE %s %s %s ORDER BY gid;" \
240 % (self.tablename, left.name, comparison, right_template)
241
242 cursor = self.db.cursor()
243 cursor.execute(query, params)
244 result = []
245 while 1:
246 row = cursor.fetchone()
247 if row is None:
248 break
249 result.append(row[0])
250 return result
251
252
253 class PostGISShape:
254
255 def __init__(self, shapeid, data):
256 self.shapeid = shapeid
257 self.data = data
258
259 def compute_bbox(self):
260 """
261 Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy)
262 """
263 xs = []
264 ys = []
265 for part in self.Points():
266 for x, y in part:
267 xs.append(x)
268 ys.append(y)
269 return (min(xs), min(ys), max(xs), max(ys))
270
271 def ShapeID(self):
272 return self.shapeid
273
274 def Points(self):
275 return wellknowntext.parse_wkt_thuban(self.data)
276
277 def RawData(self):
278 return self.data
279
280
281 shapetype_map = {"POLYGON": SHAPETYPE_POLYGON,
282 "MULTILINESTRING": SHAPETYPE_ARC,
283 "POINT": SHAPETYPE_POINT}
284
285
286 class PostGISShapeStore(PostGISTable):
287
288 """Shapestore interface to a table in a PostGIS database"""
289
290 def Table(self):
291 """Return self since a PostGISShapeStore is its own table."""
292 return self
293
294 def OrigShapeStore(self):
295 """Return None since the PostGISShapeStore is not derived from another
296 """
297 return None
298
299 def ShapeType(self):
300 """Return the type of the shapes in the shapestore."""
301 cursor = self.db.cursor()
302 cursor.execute("SELECT type FROM geometry_columns WHERE"
303 " f_table_name=%s", (self.tablename,))
304 result = cursor.fetchone()[0]
305 cursor.close()
306 return shapetype_map[result]
307
308 def RawShapeFormat(self):
309 """Return the raw data format of the shape data.
310
311 For the PostGISShapeStore this is RAW_WKT.
312 """
313 return RAW_WKT
314
315 def NumShapes(self):
316 # The number of shapes is the same as the number of rows,
317 # assuming that the geometry can't be NULL.
318 return self.NumRows()
319
320 def BoundingBox(self):
321 """Return the bounding box of all shapes in the postgis table"""
322 minx = miny = maxx = maxy = None
323 x=[]
324 y=[]
325 cursor = self.db.cursor()
326 try:
327 stmt = ("SELECT AsText(Envelope(%s)) FROM %s;"
328 % (self.geometry_column, self.tablename))
329 cursor.execute(stmt)
330 result = cursor.fetchone()
331 while result:
332 result = result[0]
333 # Here we must do some parsing through the result string
334 # to get the points out of the polygon which representes
335 # the bounding box The first and the last point of a
336 # polygon are identical
337 result = result.split("(")[2]
338 result = result.split(")")[0]
339 points = result.split(",")
340 del points[4] # Remove the last point
341 for point in points:
342 px, py = point.split()
343 x.append(float(px))
344 y.append(float(py))
345 result = cursor.fetchone()
346 finally:
347 cursor.close()
348 if not x:
349 # Empty table
350 return None
351 return (min(x), min(y), max(x), max(y))
352
353 def Shape(self, shapeid):
354 cursor = self.db.cursor()
355 cursor.execute("SELECT AsText(%s) FROM %s WHERE gid=%d"
356 % (self.geometry_column, self.tablename, shapeid))
357 wkt = cursor.fetchone()[0]
358 cursor.close()
359 return PostGISShape(shapeid, wkt)
360
361 def ShapesInRegion(self, bbox):
362 """Generate all shapes overlapping the region given by bbox."""
363 # IMPORTANT:This will work for PostGIS < 0.8
364 left, bottom, right, top = bbox
365 geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))")
366 % (left, bottom, left, top, right, top, right, bottom,
367 left, bottom))
368 cursor = self.db.cursor()
369 cursor.execute("SELECT gid, AsText(%s) FROM %s"
370 " WHERE %s && GeometryFromText('%s', -1) ORDER BY gid"
371 % (self.geometry_column, self.tablename,
372 self.geometry_column, geom))
373 while 1:
374 result = cursor.fetchone()
375 if result is None:
376 raise StopIteration
377 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