/[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 2059 - (show 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 # Copyright (C) 2003, 2004 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 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 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
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 class ConnectionError(Exception):
57
58 """Class for exceptions occurring when establishing a Databse connection"""
59
60
61 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 self.connect()
84
85 def connect(self):
86 """Internal: Establish the database connection"""
87 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 try:
93 self.connection = psycopg.connect(" ".join(params))
94 except psycopg.OperationalError, val:
95 raise ConnectionError(str(val))
96
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 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 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 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 """Column description for a PostGISTable
153
154 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 def __init__(self, name, type, index):
162 self.name = name
163 self.quoted_name = quote_identifier(name)
164 self.type = type
165 self.index = index
166
167
168 class PostGISTable:
169
170 """A Table in a PostGIS database
171
172 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 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 # 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 self.column_map = {}
191
192 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 cursor.execute("SELECT * FROM %s WHERE 0=1" % self.quoted_tablename)
199 description = cursor.description
200
201 for i in range(len(description)):
202 col = self._create_col_from_description(i, description[i])
203 if col is not None:
204 self.columns.append(col)
205
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 % (", ".join([col.quoted_name
213 for col in self.columns]),
214 self.quoted_tablename))
215
216 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 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 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 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 cursor.execute("SELECT count(*) FROM %s" % self.quoted_tablename)
270 return cursor.fetchone()[0]
271
272 def RowIdToOrdinal(self, gid):
273 """Return the row ordinal given its id"""
274 cursor = self.db.cursor()
275 cursor.execute("SELECT count(*) FROM %s WHERE gid < %d;"
276 % (self.quoted_tablename, gid))
277 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 % (self.quoted_tablename, num))
284 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 result = {}
294 for col, value in zip(self.columns, cursor.fetchone()):
295 result[col.name] = value
296 return result
297
298 def ReadValue(self, row, col, row_is_ordinal = 0):
299 cursor = self.db.cursor()
300 if row_is_ordinal:
301 stmt = ("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
302 (self.column_map[col].quoted_name, self.quoted_tablename,
303 row))
304 else:
305 stmt = ("SELECT %s FROM %s WHERE gid = %d" %
306 (self.column_map[col].quoted_name, self.quoted_tablename,
307 row))
308 cursor.execute(stmt)
309 return cursor.fetchone()[0]
310
311 def ValueRange(self, col):
312 cursor = self.db.cursor()
313 name = self.column_map[col].quoted_name
314 cursor.execute("SELECT min(%s), max(%s) FROM %s" %
315 (name, name, self.quoted_tablename))
316 return tuple(cursor.fetchone())
317
318 def UniqueValues(self, col):
319 cursor = self.db.cursor()
320 name = self.column_map[col].quoted_name
321 cursor.execute("SELECT %s FROM %s GROUP BY %s" %
322 (name, self.quoted_tablename, name))
323 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 right_template = right.quoted_name
334 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 % (self.quoted_tablename, left.quoted_name, comparison,
341 right_template)
342
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 "MULTIPOLYGON": SHAPETYPE_POLYGON,
384 "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 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 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 " WHERE f_table_name = %s AND f_geometry_column=%s",
420 (self.tablename, self.geometry_column))
421 self.srid = cursor.fetchone()[0]
422
423 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 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 " f_table_name=%s AND f_geometry_column=%s",
468 (self.tablename, self.geometry_column))
469 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 # 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 % (self.quoted_geo_col, self.quoted_tablename))
498 result = cursor.fetchone()
499 if result[0]:
500 (minx, miny), (maxx, maxy) \
501 = wellknowntext.parse_wkt_thuban(result[0])[0]
502 return (minx, miny, maxx, maxy)
503 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 % (self.quoted_geo_col, self.quoted_tablename, shapeid))
510 wkt = cursor.fetchone()[0]
511 cursor.close()
512 return PostGISShape(shapeid, wkt)
513
514 def AllShapes(self):
515 cursor = self.db.cursor()
516 cursor.execute("SELECT gid, AsText(%s) FROM %s ORDER BY gid"
517 % (self.quoted_geo_col, self.quoted_tablename))
518 while 1:
519 result = cursor.fetchone()
520 if result is None:
521 return
522 yield PostGISShape(result[0], result[1])
523
524
525 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 " WHERE %s && GeometryFromText('%s', %d) ORDER BY gid"
535 % (self.quoted_geo_col, self.quoted_tablename,
536 self.quoted_geo_col, geom, self.srid))
537 while 1:
538 result = cursor.fetchone()
539 if result is None:
540 return
541 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