/[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 2102 - (show annotations)
Thu Mar 11 21:04:30 2004 UTC (20 years, 11 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/postgisdb.py
File MIME type: text/x-python
File size: 24530 byte(s)
* Thuban/UI/dbdialog.py (ChooseDBTableDialog.__init__): Rework how
the dialog is constructed. Add combo boxes to select id and
geometry column.  Rename some instance variables.
(ChooseDBTableDialog.GetTable): Return id and geometry column
names
(ChooseDBTableDialog.OnTableSelect): New. Event handler for
selections in the table list

* Thuban/UI/mainwindow.py (MainWindow.AddDBLayer): Use id_column
and geometry_column

* Thuban/Model/session.py (Session.OpenDBShapeStore): Add the new
parameters for id_column and geometry column of PostGISShapeStore
here as well.

* Thuban/Model/postgisdb.py (type_map): Add ROWID psycog type.
(_raw_type_map): New. Map raw PostgreSQL type ints to thuban types
(PostGISConnection.GeometryTables): Use a better query to
determine which relations in the database might be usable for
shapestores.  Now supports views as well but is more PostgreSQL
specific
(PostGISConnection.table_columns): New. Somewhat experimental
method to let the db dialogs provide lists of columns to users so
that they can select id and geometry columns.
(PostGISTable.__init__): The default value of the id_column
parameter is now None it still means "gid" effectively, though.
(PostGISTable.IDColumn): New introspection method to return a
column object for the id column
(PostGISShapeStore.GeometryColumn): New introspection method to
return a column object for the geometry column

* test/test_postgis_db.py
(TestPostGISConnection.test_gis_tables_non_empty):
Removed. Subsumed by the new:
(TestPostGISConnection.test_gis_tables_with_views_and_tables):
New. Tes the GeometryTables and table_columns methods with actual
tables and views.
(PointTests.test_id_column, PointTests.test_geometry_column):
New. tests for the new methods.
(TestPostGISShapestorePoint.setUp)
(TestPostGISShapestorePointSRID.setUp)
(TestPostGISShapestorePointExplicitGIDColumn.setUp): Fill the
instance variables needed by the new tests

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 maps psycopg type objects. It's a list of pairs since
36 # the psycopg type objects are unhashable.
37 type_map = [(psycopg.STRING, table.FIELDTYPE_STRING),
38 (psycopg.INTEGER, table.FIELDTYPE_INT),
39 (psycopg.ROWID, table.FIELDTYPE_INT),
40 (psycopg.FLOAT, table.FIELDTYPE_DOUBLE)]
41
42 # _raw_type_map maps the postgresql type constants to Thuban type
43 # constants. This is very low level and postgresql specific and
44 # should be used only when necessary.
45 _raw_type_map = {}
46 def _fill_raw_type_map():
47 for psycopg_type, thuban_type in type_map:
48 for value in psycopg_type.values:
49 _raw_type_map[value] = thuban_type
50 _fill_raw_type_map()
51
52
53 def quote_identifier(ident):
54 """Return a quoted version of the identifier ident.
55
56 The return value is a string that can be put directly into an SQL
57 statement. The quoted identifier is surrounded by double quotes and
58 any double quotes already in the input value are converted to two
59 double quotes. Examples:
60
61 >>> quote_identifier("abc\"def")
62 '"abc""def"'
63 >>> quote_identifier("abc def")
64 '"abc def"'
65 """
66 return '"' + '""'.join(ident.split('"')) + '"'
67
68
69 class ConnectionError(Exception):
70
71 """Class for exceptions occurring when establishing a Databse connection"""
72
73
74 class PostGISConnection:
75
76 """Represent a PostGIS database
77
78 A PostGISConnection instance has the following public attributes:
79
80 dbname -- The name of the database
81 host, port -- Host and port to connect to
82 user -- The user name to connect as.
83
84 All of these attributes are strings and may be empty strings to
85 indicate default values.
86 """
87
88 def __init__(self, dbname, host="", user="", password="", dbtype="",
89 port=""):
90 self.dbname = dbname
91 self.host = host
92 self.port = port
93 self.user = user
94 self.password = password
95 self.dbtype = dbtype
96 self.connect()
97
98 def connect(self):
99 """Internal: Establish the database connection"""
100 params = []
101 for name in ("host", "port", "dbname", "user", "password"):
102 val = getattr(self, name)
103 if val:
104 params.append("%s=%s" % (name, val))
105 try:
106 self.connection = psycopg.connect(" ".join(params))
107 except psycopg.OperationalError, val:
108 raise ConnectionError(str(val))
109
110 # determine the OID for the geometry type. This is PostGIS
111 # specific.
112 cursor = self.connection.cursor()
113 cursor.execute("SELECT OID, typname FROM pg_type WHERE"
114 +" typname = 'geometry'")
115 row = cursor.fetchone()
116 self.connection.commit()
117 if row is not None:
118 self.geometry_type = row[0]
119 else:
120 raise ValueError("Can't determine postgres type of geometries")
121
122 def BriefDescription(self):
123 """Return a brief, one-line description of the connection
124
125 The return value is suitable for a list box of all database
126 connections.
127 """
128 return ("postgis://%(user)s@%(host)s:%(port)s/%(dbname)s"
129 % self.__dict__)
130
131 def MatchesParameters(self, parameters):
132 """Return whether the connection matches the dictionary of parameters
133
134 Return whether instatiating the connection with the given
135 parameters would establish essentially the same connection as
136 self. The connection is essentially the same if the same
137 database (identified by host, port and databasename) is accessed
138 as the same user.
139 """
140 return (parameters["host"] == self.host
141 and parameters["port"] == self.port
142 and parameters["dbname"] == self.dbname
143 and parameters["user"] == self.user)
144
145 def Close(self):
146 """Close the database connection"""
147 self.connection.close()
148
149 def GeometryTables(self):
150 """Return a list with the names of all tables with a geometry column"""
151
152 # The query is basically taken from the psql v. 7.2.1. When
153 # started with -E it prints the queries used for internal
154 # commands such as \d, which does mostly what we need here.
155 cursor = self.connection.cursor()
156 cursor.execute("SELECT c.relname FROM pg_class c"
157 " WHERE c.relkind IN ('r', 'v')"
158 # Omit the system tables
159 " AND c.relname !~ '^pg_'"
160 # Omit the special PostGIS tables
161 " AND c.relname NOT IN ('geometry_columns',"
162 " 'spatial_ref_sys')"
163 " AND %d in (SELECT a.atttypid FROM pg_attribute a"
164 " WHERE a.attrelid = c.oid)"
165 " ORDER BY c.relname;", (self.geometry_type,))
166 result = [row[0] for row in cursor.fetchall()]
167 self.connection.commit()
168 return result
169
170 def table_columns(self, tablename):
171 """Experimental: return information about the columns of a table
172
173 Return value is a list of (name, type) pairs where name is the
174 name of the column and type either one of the field type columns
175 or the string 'geometry' indicating a geometry column.
176
177 The intended use of this method is for table selection dialogs
178 which need to determine which columns are usable as id or
179 geometry columns respectively. Suitable id columns will have
180 type FIELDTYPE_INT and geometry columns will have 'geometry'.
181 """
182 result = []
183 cursor = self.connection.cursor()
184
185 # This query is taken basically from the \d command of psql
186 # 7.2.1
187 cursor.execute("SELECT a.attname, a.atttypid, a.attnum"
188 " FROM pg_class c, pg_attribute a"
189 " WHERE c.relname = %s AND a.attrelid = c.oid"
190 " ORDER BY a.attnum;", (tablename,))
191
192 for row in cursor.fetchall():
193 col_name, col_type, col_attnum = row
194 col = None
195 if col_attnum < 1:
196 # It's a system column. Only the OID is interesting
197 # here
198 if col_name == "oid":
199 col = (col_name, _raw_type_map[col_type])
200 else:
201 # If it's an integer
202 thuban_type = _raw_type_map.get(col_type)
203 if thuban_type is not None:
204 col = (col_name, thuban_type)
205 elif row[1] == self.geometry_type:
206 col = (col_name, "geometry")
207 if col is not None:
208 result.append(col)
209
210 return result
211
212 def cursor(self):
213 """Return a DB API 2.0 cursor for the database"""
214 return self.connection.cursor()
215
216
217
218 class PostGISColumn:
219
220 """Column description for a PostGISTable
221
222 In addition to the normal column object attributes name, type and
223 index, PostGISColumn objects have a quoted_name attribute which
224 contains a quoted version of name for use in SQL statements. The
225 quoted_name attribute is mainly intended for internal use by the
226 PostGISTable class.
227 """
228
229 def __init__(self, name, type, index):
230 self.name = name
231 self.quoted_name = quote_identifier(name)
232 self.type = type
233 self.index = index
234
235
236 class PostGISTable:
237
238 """A Table in a PostGIS database
239
240 A PostgreSQL table may contain columns with types not (yet)
241 supported by Thuban. Instances of this class ignore those columns
242 and pretend they don't exist, i.e. they won't show up in the column
243 descriptions returned by Columns() and other methods.
244 """
245
246 def __init__(self, db, tablename, id_column = None):
247 """Initialize the PostGISTable.
248
249 The db parameter should be an instance of PostGISConnection and
250 tablename the name of a table in the database represented by db.
251
252 The id_column parameter should be the name of a column in the
253 table that can be used to identify rows. The column must have
254 the type integer and be unique and not null.
255
256 For backwards compatibility reasons, the id_column parameter is
257 optional. If not given the table must have a column called
258 'gid' which is used as the id_column. New code should always
259 provide this parameter.
260 """
261 self.db = db
262 self.tablename = tablename
263 # Tablename quoted for use in SQL statements.
264 self.quoted_tablename = quote_identifier(tablename)
265
266 if not id_column:
267 id_column = "gid"
268 self.id_column = id_column
269 # id column name quoted for use in SQL statements.
270 self.quoted_id_column = quote_identifier(id_column)
271
272 # Map column names and indices to column objects.
273 self.column_map = {}
274
275 self._fetch_table_information()
276
277 def _fetch_table_information(self):
278 """Internal: Update information about the table"""
279 self.columns = []
280 cursor = self.db.cursor()
281 cursor.execute("SELECT * FROM %s WHERE 0=1" % self.quoted_tablename)
282 description = cursor.description
283
284 for i in range(len(description)):
285 col = self._create_col_from_description(i, description[i])
286 if col is not None:
287 self.columns.append(col)
288
289 for col in self.columns:
290 self.column_map[col.name] = col
291 self.column_map[col.index] = col
292
293 # Build query string for ReadRowAsDict
294 self.query_stmt = ("SELECT %s from %s"
295 % (", ".join([col.quoted_name
296 for col in self.columns]),
297 self.quoted_tablename))
298
299 def _create_col_from_description(self, index, description):
300 """Return the column object for the column described by description
301
302 The parameter index is the index of the column. The description
303 is a sequence taken from the cursor's description attribute for
304 the column. That means description[0] is the name of the column
305 and description[1] the type.
306
307 Return None if the column can't be represented for some reason,
308 e.g. because its type is not yet supported or needs to be
309 treated in some special way. Derived classes may extend this
310 method.
311 """
312 for pgtyp, tabletyp in type_map:
313 if pgtyp == description[1]:
314 return PostGISColumn(description[0], tabletyp,
315 len(self.columns))
316 return None
317
318 def DBConnection(self):
319 """Return the dbconnection used by the table"""
320 return self.db
321
322 def IDColumn(self):
323 """Return the column description object for the id column.
324
325 If the oid column was used as the id column, the return value is
326 not one of the regular column objects that would be returned by
327 e.g. the Column() method, but it still has meaningful name
328 attribute.
329 """
330 if self.id_column == "oid":
331 return PostGISColumn(self.id_column, table.FIELDTYPE_INT, None)
332 return self.column_map[self.id_column]
333
334 def TableName(self):
335 """Return the name of the table in the database"""
336 return self.tablename
337
338 def Title(self):
339 """Return the title of the table.
340
341 The title is currently fixed and equal to the tablename
342 """
343 return self.tablename
344
345 def Dependencies(self):
346 """Return an empty tuple because a PostGISTable depends on nothing else
347 """
348 return ()
349
350 def NumColumns(self):
351 return len(self.columns)
352
353 def Columns(self):
354 return self.columns
355
356 def Column(self, col):
357 return self.column_map[col]
358
359 def HasColumn(self, col):
360 return self.column_map.has_key(col)
361
362 def NumRows(self):
363 cursor = self.db.cursor()
364 cursor.execute("SELECT count(*) FROM %s" % self.quoted_tablename)
365 return cursor.fetchone()[0]
366
367 def RowIdToOrdinal(self, gid):
368 """Return the row ordinal given its id"""
369 cursor = self.db.cursor()
370 cursor.execute("SELECT count(*) FROM %s WHERE %s < %d;"
371 % (self.quoted_tablename, self.quoted_id_column, gid))
372 return cursor.fetchone()[0]
373
374 def RowOrdinalToId(self, num):
375 """Return the rowid for given its ordinal"""
376 cursor = self.db.cursor()
377 cursor.execute("SELECT %s FROM %s LIMIT 1 OFFSET %d;"
378 % (self.quoted_id_column, self.quoted_tablename, num))
379 return cursor.fetchone()[0]
380
381 def ReadRowAsDict(self, row, row_is_ordinal = 0):
382 cursor = self.db.cursor()
383 if row_is_ordinal:
384 stmt = self.query_stmt + " LIMIT 1 OFFSET %d" % row
385 else:
386 stmt = self.query_stmt + " WHERE %s = %d" % (self.quoted_id_column,
387 row)
388 cursor.execute(stmt)
389 result = {}
390 for col, value in zip(self.columns, cursor.fetchone()):
391 result[col.name] = value
392 return result
393
394 def ReadValue(self, row, col, row_is_ordinal = 0):
395 cursor = self.db.cursor()
396 if row_is_ordinal:
397 stmt = ("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
398 (self.column_map[col].quoted_name, self.quoted_tablename,
399 row))
400 else:
401 stmt = ("SELECT %s FROM %s WHERE %s = %d" %
402 (self.column_map[col].quoted_name, self.quoted_tablename,
403 self.quoted_id_column, row))
404 cursor.execute(stmt)
405 return cursor.fetchone()[0]
406
407 def ValueRange(self, col):
408 cursor = self.db.cursor()
409 name = self.column_map[col].quoted_name
410 cursor.execute("SELECT min(%s), max(%s) FROM %s" %
411 (name, name, self.quoted_tablename))
412 return tuple(cursor.fetchone())
413
414 def UniqueValues(self, col):
415 cursor = self.db.cursor()
416 name = self.column_map[col].quoted_name
417 cursor.execute("SELECT %s FROM %s GROUP BY %s" %
418 (name, self.quoted_tablename, name))
419 return [row[0] for row in cursor.fetchall()]
420
421 def SimpleQuery(self, left, comparison, right):
422 if comparison not in ("==", "!=", "<", "<=", ">=", ">"):
423 raise ValueError("Comparison operator %r not allowed" % comparison)
424
425 if comparison == "==":
426 comparison = "="
427
428 if isinstance(right, PostGISColumn):
429 right_template = right.quoted_name
430 params = ()
431 else:
432 right_template = "%s"
433 params = (right,)
434
435 query = "SELECT %s FROM %s WHERE %s %s %s ORDER BY %s;" \
436 % (self.quoted_id_column, self.quoted_tablename,
437 left.quoted_name, comparison, right_template,
438 self.quoted_id_column)
439
440 cursor = self.db.cursor()
441 cursor.execute(query, params)
442 result = []
443 while 1:
444 row = cursor.fetchone()
445 if row is None:
446 break
447 result.append(row[0])
448 return result
449
450
451 class PostGISShape:
452
453 def __init__(self, shapeid, data):
454 self.shapeid = shapeid
455 self.data = data
456
457 def compute_bbox(self):
458 """
459 Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy)
460 """
461 xs = []
462 ys = []
463 for part in self.Points():
464 for x, y in part:
465 xs.append(x)
466 ys.append(y)
467 return (min(xs), min(ys), max(xs), max(ys))
468
469 def ShapeID(self):
470 return self.shapeid
471
472 def Points(self):
473 return wellknowntext.parse_wkt_thuban(self.data)
474
475 def RawData(self):
476 return self.data
477
478
479 shapetype_map = {"POLYGON": SHAPETYPE_POLYGON,
480 "MULTIPOLYGON": SHAPETYPE_POLYGON,
481 "MULTILINESTRING": SHAPETYPE_ARC,
482 "POINT": SHAPETYPE_POINT}
483
484
485 class PostGISShapeStore(PostGISTable):
486
487 """Shapestore interface to a table in a PostGIS database"""
488
489 def __init__(self, db, tablename, id_column = "gid",
490 geometry_column = None):
491 """Initialize the PostGISShapeStore.
492
493 The db parameter should be an instance of PostGISConnection and
494 tablename the name of a table in the database represented by db.
495
496 The id_column parameter should be the name of a column in the
497 table that can be used to identify rows. The column must have
498 the type integer and be unique and not null.
499
500 The geometry_column paramter, if given, should be the name of
501 the geometry column to use. If the name given is not a geometry
502 column, raise a ValueError.
503
504 If no geometry_column is given, the table must have exactly one
505 geometry column. If it has more than one and the
506 geometry_column is not given, a ValueError will be raised.
507 """
508 self.geometry_column = geometry_column
509 self.geometry_column_was_given = geometry_column is not None
510 PostGISTable.__init__(self, db, tablename, id_column)
511
512 # For convenience, we have a quoted version of the geometry
513 # column in self.quoted_geo_col
514 self.quoted_geo_col = quote_identifier(self.geometry_column)
515
516 def _fetch_table_information(self):
517 """Extend inherited method to retrieve the SRID"""
518 PostGISTable._fetch_table_information(self)
519 cursor = self.db.cursor()
520 cursor.execute("SELECT srid FROM geometry_columns"
521 " WHERE f_table_name = %s AND f_geometry_column=%s",
522 (self.tablename, self.geometry_column))
523 self.srid = cursor.fetchone()[0]
524
525 def _create_col_from_description(self, index, description):
526 """Extend the inherited method to find geometry columns
527
528 If the column indicated by the parameters is a geometry column,
529 record its name in self.geometry_column and a quoted version in
530 self.quoted_geo_col. In any case return the return value of the
531 inherited method.
532 """
533 col = PostGISTable._create_col_from_description(self, index,
534 description)
535 col_name, col_type = description[:2]
536 if self.geometry_column_was_given:
537 if (col_name == self.geometry_column
538 and col_type != self.db.geometry_type):
539 raise TypeError("Column %s in %s is not a geometry column"
540 % (self.geometry_column, self.tablename))
541 else:
542 if col is None:
543 if description[1] == self.db.geometry_type:
544 # The column is a geometry column. If the name of
545 # the geometry column was not given to the
546 # constructor, and we encounter two geometry
547 # columns, raise a value error
548 if self.geometry_column is None:
549 self.geometry_column = description[0]
550 else:
551 raise TypeError("Table %s has two geometry columns"
552 " and no column name was given"
553 % (self.tablename,))
554 return col
555
556 def Table(self):
557 """Return self since a PostGISShapeStore is its own table."""
558 return self
559
560 def OrigShapeStore(self):
561 """Return None since the PostGISShapeStore is not derived from another
562 """
563 return None
564
565 def GeometryColumn(self):
566 """Return the column description object for the geometry column
567
568 There's currently no FIELDTYPE constant for this column, so the
569 return value is not a regular column object that could also be
570 returned from e.g. the Column() method. Only the name attribute
571 of the return value is meaningful at the moment.
572 """
573 return PostGISColumn(self.geometry_column, None, None)
574
575 def ShapeType(self):
576 """Return the type of the shapes in the shapestore."""
577 cursor = self.db.cursor()
578 cursor.execute("SELECT type FROM geometry_columns WHERE"
579 " f_table_name=%s AND f_geometry_column=%s",
580 (self.tablename, self.geometry_column))
581 result = cursor.fetchone()[0]
582 cursor.close()
583 return shapetype_map[result]
584
585 def RawShapeFormat(self):
586 """Return the raw data format of the shape data.
587
588 For the PostGISShapeStore this is RAW_WKT.
589 """
590 return RAW_WKT
591
592 def NumShapes(self):
593 # The number of shapes is the same as the number of rows,
594 # assuming that the geometry can't be NULL.
595 return self.NumRows()
596
597 def BoundingBox(self):
598 """Return the bounding box of all shapes in the postgis table"""
599 minx = miny = maxx = maxy = None
600 x=[]
601 y=[]
602 cursor = self.db.cursor()
603 try:
604 # Using the extent function is postgis specific. An OGC
605 # Simple Features compliant solution would be to use a query
606 # like "SELECT AsText(Envelope(the_geom)) FROM mytable;" and
607 # calculate the bounding box by hand from that
608 cursor.execute("SELECT extent(%s) FROM %s;"
609 % (self.quoted_geo_col, self.quoted_tablename))
610 result = cursor.fetchone()
611 if result[0]:
612 (minx, miny), (maxx, maxy) \
613 = wellknowntext.parse_wkt_thuban(result[0])[0]
614 return (minx, miny, maxx, maxy)
615 finally:
616 cursor.close()
617
618 def Shape(self, shapeid):
619 cursor = self.db.cursor()
620 cursor.execute("SELECT AsText(%s) FROM %s WHERE %s=%d"
621 % (self.quoted_geo_col, self.quoted_tablename,
622 self.quoted_id_column, shapeid))
623 wkt = cursor.fetchone()[0]
624 cursor.close()
625 return PostGISShape(shapeid, wkt)
626
627 def AllShapes(self):
628 cursor = self.db.cursor()
629 cursor.execute("SELECT %s, AsText(%s) FROM %s ORDER BY %s"
630 % (self.quoted_id_column, self.quoted_geo_col,
631 self.quoted_tablename, self.quoted_id_column))
632 while 1:
633 result = cursor.fetchone()
634 if result is None:
635 return
636 yield PostGISShape(result[0], result[1])
637
638
639 def ShapesInRegion(self, bbox):
640 """Generate all shapes overlapping the region given by bbox."""
641 # IMPORTANT:This will work for PostGIS < 0.8
642 left, bottom, right, top = bbox
643 geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))")
644 % (left, bottom, left, top, right, top, right, bottom,
645 left, bottom))
646 cursor = self.db.cursor()
647 cursor.execute("SELECT %(gid)s, AsText(%(geom)s) FROM %(table)s"
648 " WHERE %(geom)s && GeometryFromText('%(box)s', %(srid)d)"
649 " ORDER BY %(gid)s"
650 % {"table": self.quoted_tablename,
651 "geom": self.quoted_geo_col,
652 "gid": self.quoted_id_column,
653 "box": geom,
654 "srid": self.srid})
655 while 1:
656 result = cursor.fetchone()
657 if result is None:
658 return
659 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