/[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 1605 - (show annotations)
Tue Aug 19 11:00:40 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: 11136 byte(s)
Add very basic postgis database support and the corresponding test
cases. The test cases require a PostgreSQL + postgis installation
but no existing database. The database will be created
automatically by the test cases

* test/README: Add note about skipped tests and the requirements
of the postgis tests.

* Thuban/Model/postgisdb.py: New. Basic postgis database support.

* test/test_postgis_db.py: New. Test cases for the postgis
support.

* Thuban/Model/wellknowntext.py: New. Parser for well-known-text
format

* test/test_wellknowntext.py: New. Test cases for the
wellknowntext parser

* test/postgissupport.py: New. Support module for tests involving
a postgis database.

* test/support.py (execute_as_testsuite): Shut down the postmaster
if it's still running after the tests

* Thuban/Model/data.py (RAW_WKT): New constant for raw data in
well known text format

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