1 |
bh |
1605 |
# 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 |
bh |
1625 |
def has_postgis_support(): |
24 |
|
|
"""Return whether this Thuban instance supports PostGIS connections |
25 |
bh |
1605 |
|
26 |
bh |
1625 |
Having PostGIS support means that the psycopg module can be |
27 |
|
|
imported. |
28 |
|
|
""" |
29 |
|
|
return psycopg is not None |
30 |
|
|
|
31 |
bh |
1631 |
def psycopg_version(): |
32 |
|
|
return psycopg.__version__ |
33 |
|
|
|
34 |
bh |
1605 |
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 |
bh |
1636 |
|
40 |
|
|
class ConnectionError(Exception): |
41 |
|
|
|
42 |
|
|
"""Class for exceptions occurring when establishing a Databse connection""" |
43 |
|
|
|
44 |
|
|
|
45 |
bh |
1605 |
class PostGISConnection: |
46 |
|
|
|
47 |
|
|
"""Represent a PostGIS database |
48 |
|
|
|
49 |
|
|
A PostGISConnection instance has the following public attributes: |
50 |
|
|
|
51 |
|
|
dbname -- The name of the database |
52 |
|
|
host, port -- Host and port to connect to |
53 |
|
|
user -- The user name to connect as. |
54 |
|
|
|
55 |
|
|
All of these attributes are strings and may be empty strings to |
56 |
|
|
indicate default values. |
57 |
|
|
""" |
58 |
|
|
|
59 |
|
|
def __init__(self, dbname, host="", user="", password="", dbtype="", |
60 |
|
|
port=""): |
61 |
|
|
self.dbname = dbname |
62 |
|
|
self.host = host |
63 |
|
|
self.port = port |
64 |
|
|
self.user = user |
65 |
|
|
self.password = password |
66 |
|
|
self.dbtype = dbtype |
67 |
bh |
1620 |
self.connect() |
68 |
|
|
|
69 |
|
|
def connect(self): |
70 |
|
|
"""Internal: Establish the database connection""" |
71 |
bh |
1605 |
params = [] |
72 |
|
|
for name in ("host", "port", "dbname", "user", "password"): |
73 |
|
|
val = getattr(self, name) |
74 |
|
|
if val: |
75 |
|
|
params.append("%s=%s" % (name, val)) |
76 |
bh |
1636 |
try: |
77 |
|
|
self.connection = psycopg.connect(" ".join(params)) |
78 |
|
|
except psycopg.OperationalError, val: |
79 |
|
|
raise ConnectionError(str(val)) |
80 |
bh |
1605 |
|
81 |
|
|
# determine the OID for the geometry type. This is PostGIS |
82 |
|
|
# specific. |
83 |
|
|
cursor = self.connection.cursor() |
84 |
|
|
cursor.execute("SELECT OID, typname FROM pg_type WHERE" |
85 |
|
|
+" typname = 'geometry'") |
86 |
|
|
row = cursor.fetchone() |
87 |
|
|
self.connection.commit() |
88 |
|
|
if row is not None: |
89 |
|
|
self.geometry_type = row[0] |
90 |
|
|
else: |
91 |
|
|
raise ValueError("Can't determine postgres type of geometries") |
92 |
|
|
|
93 |
bh |
1620 |
def BriefDescription(self): |
94 |
|
|
"""Return a brief, one-line description of the connection |
95 |
|
|
|
96 |
|
|
The return value is suitable for a list box of all database |
97 |
|
|
connections. |
98 |
|
|
""" |
99 |
|
|
return ("postgis://%(user)s@%(host)s:%(port)s/%(dbname)s" |
100 |
|
|
% self.__dict__) |
101 |
|
|
|
102 |
bh |
1605 |
def Close(self): |
103 |
|
|
"""Close the database connection""" |
104 |
|
|
self.connection.close() |
105 |
|
|
|
106 |
|
|
def GeometryTables(self): |
107 |
|
|
"""Return a list with the names of all tables with a geometry column""" |
108 |
|
|
cursor = self.connection.cursor() |
109 |
|
|
cursor.execute("SELECT f_table_name FROM geometry_columns;") |
110 |
|
|
result = [row[0] for row in cursor.fetchall()] |
111 |
|
|
self.connection.commit() |
112 |
|
|
return result |
113 |
|
|
|
114 |
|
|
def cursor(self): |
115 |
|
|
"""Return a DB API 2.0 cursor for the database""" |
116 |
|
|
return self.connection.cursor() |
117 |
|
|
|
118 |
|
|
|
119 |
|
|
|
120 |
|
|
class PostGISColumn: |
121 |
|
|
|
122 |
|
|
"""Column description for a PostGISTable""" |
123 |
|
|
|
124 |
|
|
def __init__(self, name, type, index): |
125 |
|
|
self.name = name |
126 |
|
|
self.type = type |
127 |
|
|
self.index = index |
128 |
|
|
|
129 |
|
|
|
130 |
|
|
class PostGISTable: |
131 |
|
|
|
132 |
|
|
"""A Table in a PostGIS database""" |
133 |
|
|
|
134 |
|
|
def __init__(self, db, tablename): |
135 |
|
|
"""Initialize the PostGISTable. |
136 |
|
|
|
137 |
|
|
The db parameter should be an instance of PostGISConnection and |
138 |
|
|
tablename the name of a table in the database represented by db. |
139 |
|
|
""" |
140 |
|
|
self.db = db |
141 |
|
|
self.tablename = tablename |
142 |
|
|
self.column_map = {} |
143 |
|
|
self._fetch_table_information() |
144 |
|
|
|
145 |
|
|
def _fetch_table_information(self): |
146 |
|
|
"""Internal: Update information about the table""" |
147 |
|
|
self.columns = [] |
148 |
|
|
cursor = self.db.cursor() |
149 |
|
|
cursor.execute("SELECT * FROM %s WHERE 0=1" % self.tablename) |
150 |
|
|
description = cursor.description |
151 |
|
|
|
152 |
|
|
for i in range(len(description)): |
153 |
|
|
for pgtyp, tabletyp in type_map: |
154 |
|
|
if pgtyp == description[i][1]: |
155 |
|
|
col = PostGISColumn(description[i][0], tabletyp, i) |
156 |
|
|
break |
157 |
|
|
else: |
158 |
|
|
if description[i][1] == self.db.geometry_type: |
159 |
|
|
self.geometry_column = description[i][0] |
160 |
|
|
# No matching table type. Ignore the column. |
161 |
|
|
# FIXME: We should at least print a warning about |
162 |
|
|
# ignored columns |
163 |
|
|
continue |
164 |
|
|
self.columns.append(col) |
165 |
|
|
|
166 |
|
|
for col in self.columns: |
167 |
|
|
self.column_map[col.name] = col |
168 |
|
|
self.column_map[col.index] = col |
169 |
|
|
|
170 |
|
|
# Build query string for ReadRowAsDict |
171 |
|
|
self.query_stmt = ("SELECT %s from %s" |
172 |
|
|
% (", ".join([col.name for col in self.columns]), |
173 |
|
|
self.tablename)) |
174 |
|
|
|
175 |
bh |
1638 |
def DBConnection(self): |
176 |
|
|
"""Return the dbconnection used by the table""" |
177 |
|
|
return self.db |
178 |
|
|
|
179 |
|
|
def TableName(self): |
180 |
|
|
"""Return the name of the table in the database""" |
181 |
|
|
return self.tablename |
182 |
|
|
|
183 |
bh |
1658 |
def Title(self): |
184 |
|
|
"""Return the title of the table. |
185 |
|
|
|
186 |
|
|
The title is currently fixed and equal to the tablename |
187 |
|
|
""" |
188 |
|
|
return self.tablename |
189 |
|
|
|
190 |
bh |
1605 |
def Dependencies(self): |
191 |
|
|
"""Return an empty tuple because a PostGISTable depends on nothing else |
192 |
|
|
""" |
193 |
|
|
return () |
194 |
|
|
|
195 |
|
|
def NumColumns(self): |
196 |
|
|
return len(self.columns) |
197 |
|
|
|
198 |
|
|
def Columns(self): |
199 |
|
|
return self.columns |
200 |
|
|
|
201 |
|
|
def Column(self, col): |
202 |
|
|
return self.column_map[col] |
203 |
|
|
|
204 |
|
|
def HasColumn(self, col): |
205 |
|
|
return self.column_map.has_key(col) |
206 |
|
|
|
207 |
|
|
def NumRows(self): |
208 |
|
|
cursor = self.db.cursor() |
209 |
|
|
cursor.execute("SELECT count(*) FROM %s" % self.tablename) |
210 |
|
|
return cursor.fetchone()[0] |
211 |
|
|
|
212 |
bh |
1662 |
def RowIdToOrdinal(self, gid): |
213 |
|
|
"""Return the row ordinal given its id""" |
214 |
bh |
1605 |
cursor = self.db.cursor() |
215 |
bh |
1662 |
cursor.execute("SELECT count(*) FROM %s WHERE gid < %d;" |
216 |
|
|
% (self.tablename, gid)) |
217 |
|
|
return cursor.fetchone()[0] |
218 |
|
|
|
219 |
|
|
def RowOrdinalToId(self, num): |
220 |
|
|
"""Return the rowid for given its ordinal""" |
221 |
|
|
cursor = self.db.cursor() |
222 |
|
|
cursor.execute("SELECT gid FROM %s LIMIT 1 OFFSET %d;" |
223 |
|
|
% (self.tablename, num)) |
224 |
|
|
return cursor.fetchone()[0] |
225 |
|
|
|
226 |
|
|
def ReadRowAsDict(self, row, row_is_ordinal = 0): |
227 |
|
|
cursor = self.db.cursor() |
228 |
|
|
if row_is_ordinal: |
229 |
|
|
stmt = self.query_stmt + " LIMIT 1 OFFSET %d" % row |
230 |
|
|
else: |
231 |
|
|
stmt = self.query_stmt + " WHERE gid = %d" % row |
232 |
|
|
cursor.execute(stmt) |
233 |
bh |
1605 |
result = {} |
234 |
|
|
for col, value in zip(self.columns, cursor.fetchone()): |
235 |
|
|
result[col.name] = value |
236 |
|
|
return result |
237 |
|
|
|
238 |
bh |
1662 |
def ReadValue(self, row, col, row_is_ordinal = 0): |
239 |
bh |
1605 |
cursor = self.db.cursor() |
240 |
bh |
1662 |
if row_is_ordinal: |
241 |
|
|
stmt = ("SELECT %s FROM %s LIMIT 1 OFFSET %d" % |
242 |
|
|
(self.column_map[col].name, self.tablename, row)) |
243 |
|
|
else: |
244 |
|
|
stmt = ("SELECT %s FROM %s WHERE gid = %d" % |
245 |
|
|
(self.column_map[col].name, self.tablename, row)) |
246 |
|
|
cursor.execute(stmt) |
247 |
bh |
1605 |
return cursor.fetchone()[0] |
248 |
|
|
|
249 |
|
|
def ValueRange(self, col): |
250 |
|
|
cursor = self.db.cursor() |
251 |
|
|
name = self.column_map[col].name |
252 |
|
|
cursor.execute("SELECT min(%s), max(%s) FROM %s" % |
253 |
|
|
(name, name, self.tablename)) |
254 |
|
|
return tuple(cursor.fetchone()) |
255 |
|
|
|
256 |
|
|
def UniqueValues(self, col): |
257 |
|
|
cursor = self.db.cursor() |
258 |
|
|
name = self.column_map[col].name |
259 |
|
|
cursor.execute("SELECT %s FROM %s GROUP BY %s" % |
260 |
|
|
(name, self.tablename, name)) |
261 |
|
|
return [row[0] for row in cursor.fetchall()] |
262 |
|
|
|
263 |
|
|
def SimpleQuery(self, left, comparison, right): |
264 |
|
|
if comparison not in ("==", "!=", "<", "<=", ">=", ">"): |
265 |
|
|
raise ValueError("Comparison operator %r not allowed" % comparison) |
266 |
|
|
|
267 |
|
|
if comparison == "==": |
268 |
|
|
comparison = "=" |
269 |
|
|
|
270 |
|
|
if isinstance(right, PostGISColumn): |
271 |
|
|
right_template = right.name |
272 |
|
|
params = () |
273 |
|
|
else: |
274 |
|
|
right_template = "%s" |
275 |
|
|
params = (right,) |
276 |
|
|
|
277 |
|
|
query = "SELECT gid FROM %s WHERE %s %s %s ORDER BY gid;" \ |
278 |
|
|
% (self.tablename, left.name, comparison, right_template) |
279 |
|
|
|
280 |
|
|
cursor = self.db.cursor() |
281 |
|
|
cursor.execute(query, params) |
282 |
|
|
result = [] |
283 |
|
|
while 1: |
284 |
|
|
row = cursor.fetchone() |
285 |
|
|
if row is None: |
286 |
|
|
break |
287 |
|
|
result.append(row[0]) |
288 |
|
|
return result |
289 |
|
|
|
290 |
|
|
|
291 |
|
|
class PostGISShape: |
292 |
|
|
|
293 |
|
|
def __init__(self, shapeid, data): |
294 |
|
|
self.shapeid = shapeid |
295 |
|
|
self.data = data |
296 |
|
|
|
297 |
|
|
def compute_bbox(self): |
298 |
|
|
""" |
299 |
|
|
Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy) |
300 |
|
|
""" |
301 |
|
|
xs = [] |
302 |
|
|
ys = [] |
303 |
|
|
for part in self.Points(): |
304 |
|
|
for x, y in part: |
305 |
|
|
xs.append(x) |
306 |
|
|
ys.append(y) |
307 |
|
|
return (min(xs), min(ys), max(xs), max(ys)) |
308 |
|
|
|
309 |
|
|
def ShapeID(self): |
310 |
|
|
return self.shapeid |
311 |
|
|
|
312 |
|
|
def Points(self): |
313 |
|
|
return wellknowntext.parse_wkt_thuban(self.data) |
314 |
|
|
|
315 |
|
|
def RawData(self): |
316 |
|
|
return self.data |
317 |
|
|
|
318 |
|
|
|
319 |
|
|
shapetype_map = {"POLYGON": SHAPETYPE_POLYGON, |
320 |
bh |
1656 |
"MULTIPOLYGON": SHAPETYPE_POLYGON, |
321 |
bh |
1605 |
"MULTILINESTRING": SHAPETYPE_ARC, |
322 |
|
|
"POINT": SHAPETYPE_POINT} |
323 |
|
|
|
324 |
|
|
|
325 |
|
|
class PostGISShapeStore(PostGISTable): |
326 |
|
|
|
327 |
|
|
"""Shapestore interface to a table in a PostGIS database""" |
328 |
|
|
|
329 |
|
|
def Table(self): |
330 |
|
|
"""Return self since a PostGISShapeStore is its own table.""" |
331 |
|
|
return self |
332 |
|
|
|
333 |
|
|
def OrigShapeStore(self): |
334 |
|
|
"""Return None since the PostGISShapeStore is not derived from another |
335 |
|
|
""" |
336 |
|
|
return None |
337 |
|
|
|
338 |
|
|
def ShapeType(self): |
339 |
|
|
"""Return the type of the shapes in the shapestore.""" |
340 |
|
|
cursor = self.db.cursor() |
341 |
|
|
cursor.execute("SELECT type FROM geometry_columns WHERE" |
342 |
|
|
" f_table_name=%s", (self.tablename,)) |
343 |
|
|
result = cursor.fetchone()[0] |
344 |
|
|
cursor.close() |
345 |
|
|
return shapetype_map[result] |
346 |
|
|
|
347 |
|
|
def RawShapeFormat(self): |
348 |
|
|
"""Return the raw data format of the shape data. |
349 |
|
|
|
350 |
|
|
For the PostGISShapeStore this is RAW_WKT. |
351 |
|
|
""" |
352 |
|
|
return RAW_WKT |
353 |
|
|
|
354 |
|
|
def NumShapes(self): |
355 |
|
|
# The number of shapes is the same as the number of rows, |
356 |
|
|
# assuming that the geometry can't be NULL. |
357 |
|
|
return self.NumRows() |
358 |
|
|
|
359 |
|
|
def BoundingBox(self): |
360 |
|
|
"""Return the bounding box of all shapes in the postgis table""" |
361 |
|
|
minx = miny = maxx = maxy = None |
362 |
|
|
x=[] |
363 |
|
|
y=[] |
364 |
|
|
cursor = self.db.cursor() |
365 |
|
|
try: |
366 |
bh |
1660 |
# Using the extent function is postgis specific. An OGC |
367 |
|
|
# Simple Features compliant solution would be to use a query |
368 |
|
|
# like "SELECT AsText(Envelope(the_geom)) FROM mytable;" and |
369 |
|
|
# calculate the bounding box by hand from that |
370 |
|
|
cursor.execute("SELECT extent(%s) FROM %s;" |
371 |
|
|
% (self.geometry_column, self.tablename)) |
372 |
bh |
1605 |
result = cursor.fetchone() |
373 |
bh |
1660 |
if result: |
374 |
|
|
(minx, miny), (maxx, maxy) \ |
375 |
|
|
= wellknowntext.parse_wkt_thuban(result[0])[0] |
376 |
|
|
return (minx, miny, maxx, maxy) |
377 |
bh |
1605 |
finally: |
378 |
|
|
cursor.close() |
379 |
|
|
|
380 |
|
|
def Shape(self, shapeid): |
381 |
|
|
cursor = self.db.cursor() |
382 |
|
|
cursor.execute("SELECT AsText(%s) FROM %s WHERE gid=%d" |
383 |
|
|
% (self.geometry_column, self.tablename, shapeid)) |
384 |
|
|
wkt = cursor.fetchone()[0] |
385 |
|
|
cursor.close() |
386 |
|
|
return PostGISShape(shapeid, wkt) |
387 |
|
|
|
388 |
bh |
1658 |
def AllShapes(self): |
389 |
|
|
cursor = self.db.cursor() |
390 |
|
|
cursor.execute("SELECT gid, AsText(%s) FROM %s ORDER BY gid" |
391 |
|
|
% (self.geometry_column, self.tablename)) |
392 |
|
|
while 1: |
393 |
|
|
result = cursor.fetchone() |
394 |
|
|
if result is None: |
395 |
|
|
return |
396 |
|
|
yield PostGISShape(result[0], result[1]) |
397 |
|
|
|
398 |
|
|
|
399 |
bh |
1605 |
def ShapesInRegion(self, bbox): |
400 |
|
|
"""Generate all shapes overlapping the region given by bbox.""" |
401 |
|
|
# IMPORTANT:This will work for PostGIS < 0.8 |
402 |
|
|
left, bottom, right, top = bbox |
403 |
|
|
geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))") |
404 |
|
|
% (left, bottom, left, top, right, top, right, bottom, |
405 |
|
|
left, bottom)) |
406 |
|
|
cursor = self.db.cursor() |
407 |
|
|
cursor.execute("SELECT gid, AsText(%s) FROM %s" |
408 |
|
|
" WHERE %s && GeometryFromText('%s', -1) ORDER BY gid" |
409 |
|
|
% (self.geometry_column, self.tablename, |
410 |
|
|
self.geometry_column, geom)) |
411 |
|
|
while 1: |
412 |
|
|
result = cursor.fetchone() |
413 |
|
|
if result is None: |
414 |
bh |
1658 |
return |
415 |
bh |
1605 |
yield PostGISShape(result[0], result[1]) |