/[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 1620 - (show annotations)
Wed Aug 20 13:14:22 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: 11541 byte(s)
Add dialogs and commands to open database connections and add
database layers.

* Thuban/UI/mainwindow.py (MainWindow.DatabaseManagement): New
method to open the database connection management dialog
(MainWindow.AddDBLayer): New method to add a layer from a database
(_has_dbconnections): New helper function to use for sensitivity
(database_management command, layer_add_db command): New commands
that call the above new methods.
(main_menu): Add the new commands to the menu.

* Thuban/Model/postgisdb.py (PostGISConnection.__init__)
(PostGISConnection.connect): Establish the actual connection in a
separate method and call it in __init__. This makes it easier to
override the behavior in test cases
(PostGISConnection.BriefDescription): New method to return a brief
description for use in dialogs.

* test/test_postgis_db.py (NonConnection): DB connection that
doesn't actually connect
(TestBriefDescription): New class with tests for the new
BriefDescription method

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
24 if psycopg is not None:
25 type_map = [(psycopg.STRING, table.FIELDTYPE_STRING),
26 (psycopg.INTEGER, table.FIELDTYPE_INT),
27 (psycopg.FLOAT, table.FIELDTYPE_DOUBLE)]
28
29 class PostGISConnection:
30
31 """Represent a PostGIS database
32
33 A PostGISConnection instance has the following public attributes:
34
35 dbname -- The name of the database
36 host, port -- Host and port to connect to
37 user -- The user name to connect as.
38
39 All of these attributes are strings and may be empty strings to
40 indicate default values.
41 """
42
43 def __init__(self, dbname, host="", user="", password="", dbtype="",
44 port=""):
45 self.dbname = dbname
46 self.host = host
47 self.port = port
48 self.user = user
49 self.password = password
50 self.dbtype = dbtype
51 self.connect()
52
53 def connect(self):
54 """Internal: Establish the database connection"""
55 params = []
56 for name in ("host", "port", "dbname", "user", "password"):
57 val = getattr(self, name)
58 if val:
59 params.append("%s=%s" % (name, val))
60 self.connection = psycopg.connect(" ".join(params))
61
62 # determine the OID for the geometry type. This is PostGIS
63 # specific.
64 cursor = self.connection.cursor()
65 cursor.execute("SELECT OID, typname FROM pg_type WHERE"
66 +" typname = 'geometry'")
67 row = cursor.fetchone()
68 self.connection.commit()
69 if row is not None:
70 self.geometry_type = row[0]
71 else:
72 raise ValueError("Can't determine postgres type of geometries")
73
74 def BriefDescription(self):
75 """Return a brief, one-line description of the connection
76
77 The return value is suitable for a list box of all database
78 connections.
79 """
80 return ("postgis://%(user)s@%(host)s:%(port)s/%(dbname)s"
81 % self.__dict__)
82
83 def Close(self):
84 """Close the database connection"""
85 self.connection.close()
86
87 def GeometryTables(self):
88 """Return a list with the names of all tables with a geometry column"""
89 cursor = self.connection.cursor()
90 cursor.execute("SELECT f_table_name FROM geometry_columns;")
91 result = [row[0] for row in cursor.fetchall()]
92 self.connection.commit()
93 return result
94
95 def cursor(self):
96 """Return a DB API 2.0 cursor for the database"""
97 return self.connection.cursor()
98
99
100
101 class PostGISColumn:
102
103 """Column description for a PostGISTable"""
104
105 def __init__(self, name, type, index):
106 self.name = name
107 self.type = type
108 self.index = index
109
110
111 class PostGISTable:
112
113 """A Table in a PostGIS database"""
114
115 def __init__(self, db, tablename):
116 """Initialize the PostGISTable.
117
118 The db parameter should be an instance of PostGISConnection and
119 tablename the name of a table in the database represented by db.
120 """
121 self.db = db
122 self.tablename = tablename
123 self.column_map = {}
124 self._fetch_table_information()
125
126 def _fetch_table_information(self):
127 """Internal: Update information about the table"""
128 self.columns = []
129 cursor = self.db.cursor()
130 cursor.execute("SELECT * FROM %s WHERE 0=1" % self.tablename)
131 description = cursor.description
132
133 for i in range(len(description)):
134 for pgtyp, tabletyp in type_map:
135 if pgtyp == description[i][1]:
136 col = PostGISColumn(description[i][0], tabletyp, i)
137 break
138 else:
139 if description[i][1] == self.db.geometry_type:
140 self.geometry_column = description[i][0]
141 # No matching table type. Ignore the column.
142 # FIXME: We should at least print a warning about
143 # ignored columns
144 continue
145 self.columns.append(col)
146
147 for col in self.columns:
148 self.column_map[col.name] = col
149 self.column_map[col.index] = col
150
151 # Build query string for ReadRowAsDict
152 self.query_stmt = ("SELECT %s from %s"
153 % (", ".join([col.name for col in self.columns]),
154 self.tablename))
155
156 def Dependencies(self):
157 """Return an empty tuple because a PostGISTable depends on nothing else
158 """
159 return ()
160
161 def NumColumns(self):
162 return len(self.columns)
163
164 def Columns(self):
165 return self.columns
166
167 def Column(self, col):
168 return self.column_map[col]
169
170 def HasColumn(self, col):
171 return self.column_map.has_key(col)
172
173 def NumRows(self):
174 cursor = self.db.cursor()
175 cursor.execute("SELECT count(*) FROM %s" % self.tablename)
176 return cursor.fetchone()[0]
177
178 def ReadRowAsDict(self, row):
179 cursor = self.db.cursor()
180 cursor.execute(self.query_stmt + " LIMIT 1 OFFSET %d" % row)
181 result = {}
182 for col, value in zip(self.columns, cursor.fetchone()):
183 result[col.name] = value
184 return result
185
186 def ReadValue(self, row, col):
187 cursor = self.db.cursor()
188 cursor.execute("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
189 (self.column_map[col].name, self.tablename, row))
190 return cursor.fetchone()[0]
191
192 def ValueRange(self, col):
193 cursor = self.db.cursor()
194 name = self.column_map[col].name
195 cursor.execute("SELECT min(%s), max(%s) FROM %s" %
196 (name, name, self.tablename))
197 return tuple(cursor.fetchone())
198
199 def UniqueValues(self, col):
200 cursor = self.db.cursor()
201 name = self.column_map[col].name
202 cursor.execute("SELECT %s FROM %s GROUP BY %s" %
203 (name, self.tablename, name))
204 return [row[0] for row in cursor.fetchall()]
205
206 def SimpleQuery(self, left, comparison, right):
207 if comparison not in ("==", "!=", "<", "<=", ">=", ">"):
208 raise ValueError("Comparison operator %r not allowed" % comparison)
209
210 if comparison == "==":
211 comparison = "="
212
213 if isinstance(right, PostGISColumn):
214 right_template = right.name
215 params = ()
216 else:
217 right_template = "%s"
218 params = (right,)
219
220 query = "SELECT gid FROM %s WHERE %s %s %s ORDER BY gid;" \
221 % (self.tablename, left.name, comparison, right_template)
222
223 cursor = self.db.cursor()
224 cursor.execute(query, params)
225 result = []
226 while 1:
227 row = cursor.fetchone()
228 if row is None:
229 break
230 result.append(row[0])
231 return result
232
233
234 class PostGISShape:
235
236 def __init__(self, shapeid, data):
237 self.shapeid = shapeid
238 self.data = data
239
240 def compute_bbox(self):
241 """
242 Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy)
243 """
244 xs = []
245 ys = []
246 for part in self.Points():
247 for x, y in part:
248 xs.append(x)
249 ys.append(y)
250 return (min(xs), min(ys), max(xs), max(ys))
251
252 def ShapeID(self):
253 return self.shapeid
254
255 def Points(self):
256 return wellknowntext.parse_wkt_thuban(self.data)
257
258 def RawData(self):
259 return self.data
260
261
262 shapetype_map = {"POLYGON": SHAPETYPE_POLYGON,
263 "MULTILINESTRING": SHAPETYPE_ARC,
264 "POINT": SHAPETYPE_POINT}
265
266
267 class PostGISShapeStore(PostGISTable):
268
269 """Shapestore interface to a table in a PostGIS database"""
270
271 def Table(self):
272 """Return self since a PostGISShapeStore is its own table."""
273 return self
274
275 def OrigShapeStore(self):
276 """Return None since the PostGISShapeStore is not derived from another
277 """
278 return None
279
280 def ShapeType(self):
281 """Return the type of the shapes in the shapestore."""
282 cursor = self.db.cursor()
283 cursor.execute("SELECT type FROM geometry_columns WHERE"
284 " f_table_name=%s", (self.tablename,))
285 result = cursor.fetchone()[0]
286 cursor.close()
287 return shapetype_map[result]
288
289 def RawShapeFormat(self):
290 """Return the raw data format of the shape data.
291
292 For the PostGISShapeStore this is RAW_WKT.
293 """
294 return RAW_WKT
295
296 def NumShapes(self):
297 # The number of shapes is the same as the number of rows,
298 # assuming that the geometry can't be NULL.
299 return self.NumRows()
300
301 def BoundingBox(self):
302 """Return the bounding box of all shapes in the postgis table"""
303 minx = miny = maxx = maxy = None
304 x=[]
305 y=[]
306 cursor = self.db.cursor()
307 try:
308 stmt = ("SELECT AsText(Envelope(%s)) FROM %s;"
309 % (self.geometry_column, self.tablename))
310 cursor.execute(stmt)
311 result = cursor.fetchone()
312 while result:
313 result = result[0]
314 # Here we must do some parsing through the result string
315 # to get the points out of the polygon which representes
316 # the bounding box The first and the last point of a
317 # polygon are identical
318 result = result.split("(")[2]
319 result = result.split(")")[0]
320 points = result.split(",")
321 del points[4] # Remove the last point
322 for point in points:
323 px, py = point.split()
324 x.append(float(px))
325 y.append(float(py))
326 result = cursor.fetchone()
327 finally:
328 cursor.close()
329 if not x:
330 # Empty table
331 return None
332 return (min(x), min(y), max(x), max(y))
333
334 def Shape(self, shapeid):
335 cursor = self.db.cursor()
336 cursor.execute("SELECT AsText(%s) FROM %s WHERE gid=%d"
337 % (self.geometry_column, self.tablename, shapeid))
338 wkt = cursor.fetchone()[0]
339 cursor.close()
340 return PostGISShape(shapeid, wkt)
341
342 def ShapesInRegion(self, bbox):
343 """Generate all shapes overlapping the region given by bbox."""
344 # IMPORTANT:This will work for PostGIS < 0.8
345 left, bottom, right, top = bbox
346 geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))")
347 % (left, bottom, left, top, right, top, right, bottom,
348 left, bottom))
349 cursor = self.db.cursor()
350 cursor.execute("SELECT gid, AsText(%s) FROM %s"
351 " WHERE %s && GeometryFromText('%s', -1) ORDER BY gid"
352 % (self.geometry_column, self.tablename,
353 self.geometry_column, geom))
354 while 1:
355 result = cursor.fetchone()
356 if result is None:
357 raise StopIteration
358 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