/[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 2472 - (show annotations)
Thu Dec 16 15:18:57 2004 UTC (20 years, 2 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/postgisdb.py
File MIME type: text/x-python
File size: 25737 byte(s)
Add support for PostGIS tables with LINESTRING geomentries.
Fixes RT#2299

* Thuban/Model/postgisdb.py (shapetype_map): Add LINESTRING

* test/postgissupport.py
(PostgreSQLServer.get_default_static_data_db): Rename the "roads"
table to "roads-multi" because it now uses MULTILINESTRING
geometries and introduce a new "roads" table that uses LINESTRING
(coords_to_multilinestring): Make the doc string more precise
(coords_to_linestring): New.  Create a LINESTRING WKT
representatin
(wkt_converter): Add coords_to_linestring
(upload_shapefile): Rephrase the doc-string a bit.

* test/test_postgis_db.py (TestPostGISShapestoreArc)
(LineStringTests)
(TestPostGISShapestoreLineString)
(TestPostGISShapestoreMultiLineString): Split
TestPostGISShapestoreArc into a base class LineStringTests and two
derived classes TestPostGISShapestoreLineString for LINESTRING
geometries and TestPostGISShapestoreMultiLineString for
MULTILINESTRING geometries.  Most test methods are in the base
class with the exception of tests that explicitly check the raw
format.

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 "LINESTRING": SHAPETYPE_ARC,
482 "MULTILINESTRING": SHAPETYPE_ARC,
483 "POINT": SHAPETYPE_POINT}
484
485
486 class PostGISShapeStore(PostGISTable):
487
488 """Shapestore interface to a table in a PostGIS database"""
489
490 def __init__(self, db, tablename, id_column = "gid",
491 geometry_column = None):
492 """Initialize the PostGISShapeStore.
493
494 The db parameter should be an instance of PostGISConnection and
495 tablename the name of a table in the database represented by db.
496
497 The id_column parameter should be the name of a column in the
498 table that can be used to identify rows. The column must have
499 the type integer and be unique and not null.
500
501 The geometry_column paramter, if given, should be the name of
502 the geometry column to use. If the name given is not a geometry
503 column, raise a ValueError.
504
505 If no geometry_column is given, the table must have exactly one
506 geometry column. If it has more than one and the
507 geometry_column is not given, a ValueError will be raised.
508 """
509 self.geometry_column = geometry_column
510 self.geometry_column_was_given = geometry_column is not None
511 PostGISTable.__init__(self, db, tablename, id_column)
512
513 # For convenience, we have a quoted version of the geometry
514 # column in self.quoted_geo_col
515 self.quoted_geo_col = quote_identifier(self.geometry_column)
516
517 def _fetch_table_information(self):
518 """Extend inherited method to retrieve the SRID and shape type"""
519 PostGISTable._fetch_table_information(self)
520
521 # First, try to get it from the geometry_columns table.
522 cursor = self.db.cursor()
523 cursor.execute("SELECT srid, type FROM geometry_columns"
524 " WHERE f_table_name = %s AND f_geometry_column=%s",
525 (self.tablename, self.geometry_column))
526 row = cursor.fetchone()
527 if row is not None:
528 self.srid = row[0]
529 self.shape_type = shapetype_map.get(row[1])
530 return
531
532 # The table is probably really a view and thus not in
533 # geometry_columns. Use a different approach
534 cursor = self.db.cursor()
535 cursor.execute("SELECT DISTINCT SRID(%s) FROM %s;" %
536 (quote_identifier(self.geometry_column),
537 self.tablename))
538 row = cursor.fetchone()
539 if row is not None:
540 self.srid = row[0]
541 # Try to see whether there's another one
542 row = cursor.fetchone()
543 if row is not None:
544 # There are at least two different srids. We don't
545 # support that
546 self.srid = None
547
548 cursor = self.db.cursor()
549 cursor.execute("SELECT DISTINCT GeometryType(%s) FROM %s;"
550 % (quote_identifier(self.geometry_column),
551 self.tablename))
552 row = cursor.fetchone()
553 if row is not None:
554 self.shape_type = shapetype_map.get(row[0])
555 # Try to see whether there's another one
556 row = cursor.fetchone()
557 if row is not None:
558 # There are at least two different srids. We don't
559 # support that
560 self.shape_type = None
561
562 def _create_col_from_description(self, index, description):
563 """Extend the inherited method to find geometry columns
564
565 If the column indicated by the parameters is a geometry column,
566 record its name in self.geometry_column and a quoted version in
567 self.quoted_geo_col. In any case return the return value of the
568 inherited method.
569 """
570 col = PostGISTable._create_col_from_description(self, index,
571 description)
572 col_name, col_type = description[:2]
573 if self.geometry_column_was_given:
574 if (col_name == self.geometry_column
575 and col_type != self.db.geometry_type):
576 raise TypeError("Column %s in %s is not a geometry column"
577 % (self.geometry_column, self.tablename))
578 else:
579 if col is None:
580 if description[1] == self.db.geometry_type:
581 # The column is a geometry column. If the name of
582 # the geometry column was not given to the
583 # constructor, and we encounter two geometry
584 # columns, raise a value error
585 if self.geometry_column is None:
586 self.geometry_column = description[0]
587 else:
588 raise TypeError("Table %s has two geometry columns"
589 " and no column name was given"
590 % (self.tablename,))
591 return col
592
593 def Table(self):
594 """Return self since a PostGISShapeStore is its own table."""
595 return self
596
597 def OrigShapeStore(self):
598 """Return None since the PostGISShapeStore is not derived from another
599 """
600 return None
601
602 def GeometryColumn(self):
603 """Return the column description object for the geometry column
604
605 There's currently no FIELDTYPE constant for this column, so the
606 return value is not a regular column object that could also be
607 returned from e.g. the Column() method. Only the name attribute
608 of the return value is meaningful at the moment.
609 """
610 return PostGISColumn(self.geometry_column, None, None)
611
612 def ShapeType(self):
613 """Return the type of the shapes in the shapestore."""
614 return self.shape_type
615
616 def RawShapeFormat(self):
617 """Return the raw data format of the shape data.
618
619 For the PostGISShapeStore this is RAW_WKT.
620 """
621 return RAW_WKT
622
623 def NumShapes(self):
624 # The number of shapes is the same as the number of rows,
625 # assuming that the geometry can't be NULL.
626 return self.NumRows()
627
628 def BoundingBox(self):
629 """Return the bounding box of all shapes in the postgis table"""
630 minx = miny = maxx = maxy = None
631 x=[]
632 y=[]
633 cursor = self.db.cursor()
634 try:
635 # Using the extent function is postgis specific. An OGC
636 # Simple Features compliant solution would be to use a query
637 # like "SELECT AsText(Envelope(the_geom)) FROM mytable;" and
638 # calculate the bounding box by hand from that
639 cursor.execute("SELECT extent(%s) FROM %s;"
640 % (self.quoted_geo_col, self.quoted_tablename))
641 result = cursor.fetchone()
642 if result[0]:
643 (minx, miny), (maxx, maxy) \
644 = wellknowntext.parse_wkt_thuban(result[0])[0]
645 return (minx, miny, maxx, maxy)
646 finally:
647 cursor.close()
648
649 def Shape(self, shapeid):
650 cursor = self.db.cursor()
651 cursor.execute("SELECT AsText(%s) FROM %s WHERE %s=%d"
652 % (self.quoted_geo_col, self.quoted_tablename,
653 self.quoted_id_column, shapeid))
654 wkt = cursor.fetchone()[0]
655 cursor.close()
656 return PostGISShape(shapeid, wkt)
657
658 def AllShapes(self):
659 cursor = self.db.cursor()
660 cursor.execute("SELECT %s, AsText(%s) FROM %s ORDER BY %s"
661 % (self.quoted_id_column, self.quoted_geo_col,
662 self.quoted_tablename, self.quoted_id_column))
663 while 1:
664 result = cursor.fetchone()
665 if result is None:
666 return
667 yield PostGISShape(result[0], result[1])
668
669
670 def ShapesInRegion(self, bbox):
671 """Generate all shapes overlapping the region given by bbox."""
672 # IMPORTANT:This will work for PostGIS < 0.8
673 left, bottom, right, top = bbox
674 geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))")
675 % (left, bottom, left, top, right, top, right, bottom,
676 left, bottom))
677 cursor = self.db.cursor()
678 cursor.execute("SELECT %(gid)s, AsText(%(geom)s) FROM %(table)s"
679 " WHERE %(geom)s && GeometryFromText('%(box)s', %(srid)d)"
680 " ORDER BY %(gid)s"
681 % {"table": self.quoted_tablename,
682 "geom": self.quoted_geo_col,
683 "gid": self.quoted_id_column,
684 "box": geom,
685 "srid": self.srid})
686 while 1:
687 result = cursor.fetchone()
688 if result is None:
689 return
690 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