/[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 1693 - (show 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 # 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 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 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 col = PostGISColumn(description[i][0], tabletyp,
162 len(self.columns))
163 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 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 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 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 def RowIdToOrdinal(self, gid):
220 """Return the row ordinal given its id"""
221 cursor = self.db.cursor()
222 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 result = {}
241 for col, value in zip(self.columns, cursor.fetchone()):
242 result[col.name] = value
243 return result
244
245 def ReadValue(self, row, col, row_is_ordinal = 0):
246 cursor = self.db.cursor()
247 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 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 "MULTIPOLYGON": SHAPETYPE_POLYGON,
328 "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 # 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 result = cursor.fetchone()
380 if result:
381 (minx, miny), (maxx, maxy) \
382 = wellknowntext.parse_wkt_thuban(result[0])[0]
383 return (minx, miny, maxx, maxy)
384 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 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 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 return
422 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