1 |
bh |
2057 |
# Copyright (C) 2003, 2004 by Intevation GmbH |
2 |
bh |
1605 |
# 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 |
bh |
2102 |
# type_map maps psycopg type objects. It's a list of pairs since |
36 |
|
|
# the psycopg type objects are unhashable. |
37 |
bh |
1605 |
type_map = [(psycopg.STRING, table.FIELDTYPE_STRING), |
38 |
|
|
(psycopg.INTEGER, table.FIELDTYPE_INT), |
39 |
bh |
2102 |
(psycopg.ROWID, table.FIELDTYPE_INT), |
40 |
bh |
1605 |
(psycopg.FLOAT, table.FIELDTYPE_DOUBLE)] |
41 |
|
|
|
42 |
bh |
2102 |
# _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 |
bh |
1636 |
|
52 |
bh |
2102 |
|
53 |
bh |
1946 |
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 |
bh |
2057 |
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 |
bh |
1946 |
|
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 |
bh |
1636 |
class ConnectionError(Exception): |
70 |
|
|
|
71 |
|
|
"""Class for exceptions occurring when establishing a Databse connection""" |
72 |
|
|
|
73 |
|
|
|
74 |
bh |
1605 |
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 |
bh |
1620 |
self.connect() |
97 |
|
|
|
98 |
|
|
def connect(self): |
99 |
|
|
"""Internal: Establish the database connection""" |
100 |
bh |
1605 |
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 |
bh |
1636 |
try: |
106 |
|
|
self.connection = psycopg.connect(" ".join(params)) |
107 |
|
|
except psycopg.OperationalError, val: |
108 |
|
|
raise ConnectionError(str(val)) |
109 |
bh |
1605 |
|
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 |
bh |
1620 |
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 |
bh |
1955 |
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 |
bh |
1605 |
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 |
bh |
2102 |
|
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 |
bh |
1605 |
cursor = self.connection.cursor() |
156 |
bh |
2102 |
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 |
bh |
1605 |
result = [row[0] for row in cursor.fetchall()] |
167 |
|
|
self.connection.commit() |
168 |
|
|
return result |
169 |
|
|
|
170 |
bh |
2102 |
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 |
bh |
1605 |
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 |
bh |
1946 |
"""Column description for a PostGISTable |
221 |
bh |
1605 |
|
222 |
bh |
1946 |
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 |
bh |
1605 |
def __init__(self, name, type, index): |
230 |
|
|
self.name = name |
231 |
bh |
1946 |
self.quoted_name = quote_identifier(name) |
232 |
bh |
1605 |
self.type = type |
233 |
|
|
self.index = index |
234 |
|
|
|
235 |
|
|
|
236 |
|
|
class PostGISTable: |
237 |
|
|
|
238 |
bh |
1693 |
"""A Table in a PostGIS database |
239 |
bh |
1605 |
|
240 |
bh |
1693 |
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 |
bh |
2102 |
def __init__(self, db, tablename, id_column = None): |
247 |
bh |
1605 |
"""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 |
bh |
2096 |
|
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 |
bh |
2102 |
|
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 |
bh |
1605 |
""" |
261 |
|
|
self.db = db |
262 |
|
|
self.tablename = tablename |
263 |
bh |
1946 |
# Tablename quoted for use in SQL statements. |
264 |
|
|
self.quoted_tablename = quote_identifier(tablename) |
265 |
|
|
|
266 |
bh |
2102 |
if not id_column: |
267 |
|
|
id_column = "gid" |
268 |
bh |
2096 |
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 |
bh |
1946 |
# Map column names and indices to column objects. |
273 |
bh |
1605 |
self.column_map = {} |
274 |
bh |
1946 |
|
275 |
bh |
1605 |
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 |
bh |
1946 |
cursor.execute("SELECT * FROM %s WHERE 0=1" % self.quoted_tablename) |
282 |
bh |
1605 |
description = cursor.description |
283 |
|
|
|
284 |
|
|
for i in range(len(description)): |
285 |
bh |
2059 |
col = self._create_col_from_description(i, description[i]) |
286 |
|
|
if col is not None: |
287 |
|
|
self.columns.append(col) |
288 |
bh |
1605 |
|
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 |
bh |
1946 |
% (", ".join([col.quoted_name |
296 |
|
|
for col in self.columns]), |
297 |
|
|
self.quoted_tablename)) |
298 |
bh |
1605 |
|
299 |
bh |
2059 |
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 |
bh |
1638 |
def DBConnection(self): |
319 |
|
|
"""Return the dbconnection used by the table""" |
320 |
|
|
return self.db |
321 |
|
|
|
322 |
bh |
2102 |
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 |
bh |
1638 |
def TableName(self): |
335 |
|
|
"""Return the name of the table in the database""" |
336 |
|
|
return self.tablename |
337 |
|
|
|
338 |
bh |
1658 |
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 |
bh |
1605 |
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 |
bh |
1946 |
cursor.execute("SELECT count(*) FROM %s" % self.quoted_tablename) |
365 |
bh |
1605 |
return cursor.fetchone()[0] |
366 |
|
|
|
367 |
bh |
1662 |
def RowIdToOrdinal(self, gid): |
368 |
|
|
"""Return the row ordinal given its id""" |
369 |
bh |
1605 |
cursor = self.db.cursor() |
370 |
bh |
2096 |
cursor.execute("SELECT count(*) FROM %s WHERE %s < %d;" |
371 |
|
|
% (self.quoted_tablename, self.quoted_id_column, gid)) |
372 |
bh |
1662 |
return cursor.fetchone()[0] |
373 |
|
|
|
374 |
|
|
def RowOrdinalToId(self, num): |
375 |
|
|
"""Return the rowid for given its ordinal""" |
376 |
|
|
cursor = self.db.cursor() |
377 |
bh |
2096 |
cursor.execute("SELECT %s FROM %s LIMIT 1 OFFSET %d;" |
378 |
|
|
% (self.quoted_id_column, self.quoted_tablename, num)) |
379 |
bh |
1662 |
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 |
bh |
2096 |
stmt = self.query_stmt + " WHERE %s = %d" % (self.quoted_id_column, |
387 |
|
|
row) |
388 |
bh |
1662 |
cursor.execute(stmt) |
389 |
bh |
1605 |
result = {} |
390 |
|
|
for col, value in zip(self.columns, cursor.fetchone()): |
391 |
|
|
result[col.name] = value |
392 |
|
|
return result |
393 |
|
|
|
394 |
bh |
1662 |
def ReadValue(self, row, col, row_is_ordinal = 0): |
395 |
bh |
1605 |
cursor = self.db.cursor() |
396 |
bh |
1662 |
if row_is_ordinal: |
397 |
|
|
stmt = ("SELECT %s FROM %s LIMIT 1 OFFSET %d" % |
398 |
bh |
1946 |
(self.column_map[col].quoted_name, self.quoted_tablename, |
399 |
|
|
row)) |
400 |
bh |
1662 |
else: |
401 |
bh |
2096 |
stmt = ("SELECT %s FROM %s WHERE %s = %d" % |
402 |
bh |
1946 |
(self.column_map[col].quoted_name, self.quoted_tablename, |
403 |
bh |
2096 |
self.quoted_id_column, row)) |
404 |
bh |
1662 |
cursor.execute(stmt) |
405 |
bh |
1605 |
return cursor.fetchone()[0] |
406 |
|
|
|
407 |
|
|
def ValueRange(self, col): |
408 |
|
|
cursor = self.db.cursor() |
409 |
bh |
1946 |
name = self.column_map[col].quoted_name |
410 |
bh |
1605 |
cursor.execute("SELECT min(%s), max(%s) FROM %s" % |
411 |
bh |
1946 |
(name, name, self.quoted_tablename)) |
412 |
bh |
1605 |
return tuple(cursor.fetchone()) |
413 |
|
|
|
414 |
|
|
def UniqueValues(self, col): |
415 |
|
|
cursor = self.db.cursor() |
416 |
bh |
1946 |
name = self.column_map[col].quoted_name |
417 |
bh |
1605 |
cursor.execute("SELECT %s FROM %s GROUP BY %s" % |
418 |
bh |
1946 |
(name, self.quoted_tablename, name)) |
419 |
bh |
1605 |
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 |
bh |
1946 |
right_template = right.quoted_name |
430 |
bh |
1605 |
params = () |
431 |
|
|
else: |
432 |
|
|
right_template = "%s" |
433 |
|
|
params = (right,) |
434 |
|
|
|
435 |
bh |
2096 |
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 |
bh |
1605 |
|
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 |
bh |
1656 |
"MULTIPOLYGON": SHAPETYPE_POLYGON, |
481 |
bh |
1605 |
"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 |
bh |
2096 |
def __init__(self, db, tablename, id_column = "gid", |
490 |
|
|
geometry_column = None): |
491 |
bh |
2059 |
"""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 |
bh |
2096 |
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 |
bh |
2059 |
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 |
bh |
2096 |
PostGISTable.__init__(self, db, tablename, id_column) |
511 |
bh |
2059 |
|
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 |
bh |
2057 |
def _fetch_table_information(self): |
517 |
bh |
2106 |
"""Extend inherited method to retrieve the SRID and shape type""" |
518 |
bh |
2057 |
PostGISTable._fetch_table_information(self) |
519 |
bh |
2106 |
|
520 |
|
|
# First, try to get it from the geometry_columns table. |
521 |
bh |
2057 |
cursor = self.db.cursor() |
522 |
bh |
2106 |
cursor.execute("SELECT srid, type FROM geometry_columns" |
523 |
bh |
2059 |
" WHERE f_table_name = %s AND f_geometry_column=%s", |
524 |
|
|
(self.tablename, self.geometry_column)) |
525 |
bh |
2106 |
row = cursor.fetchone() |
526 |
|
|
if row is not None: |
527 |
|
|
self.srid = row[0] |
528 |
|
|
self.shape_type = shapetype_map.get(row[1]) |
529 |
|
|
return |
530 |
bh |
2057 |
|
531 |
bh |
2106 |
# The table is probably really a view and thus not in |
532 |
|
|
# geometry_columns. Use a different approach |
533 |
|
|
cursor = self.db.cursor() |
534 |
|
|
cursor.execute("SELECT DISTINCT SRID(%s) FROM %s;" % |
535 |
|
|
(quote_identifier(self.geometry_column), |
536 |
|
|
self.tablename)) |
537 |
|
|
row = cursor.fetchone() |
538 |
|
|
if row is not None: |
539 |
|
|
self.srid = row[0] |
540 |
|
|
# Try to see whether there's another one |
541 |
|
|
row = cursor.fetchone() |
542 |
|
|
if row is not None: |
543 |
|
|
# There are at least two different srids. We don't |
544 |
|
|
# support that |
545 |
|
|
self.srid = None |
546 |
|
|
|
547 |
|
|
cursor = self.db.cursor() |
548 |
|
|
cursor.execute("SELECT DISTINCT GeometryType(%s) FROM %s;" |
549 |
|
|
% (quote_identifier(self.geometry_column), |
550 |
|
|
self.tablename)) |
551 |
|
|
row = cursor.fetchone() |
552 |
|
|
if row is not None: |
553 |
|
|
self.shape_type = shapetype_map.get(row[0]) |
554 |
|
|
# Try to see whether there's another one |
555 |
|
|
row = cursor.fetchone() |
556 |
|
|
if row is not None: |
557 |
|
|
# There are at least two different srids. We don't |
558 |
|
|
# support that |
559 |
|
|
self.shape_type = None |
560 |
|
|
|
561 |
bh |
2059 |
def _create_col_from_description(self, index, description): |
562 |
|
|
"""Extend the inherited method to find geometry columns |
563 |
|
|
|
564 |
bh |
2096 |
If the column indicated by the parameters is a geometry column, |
565 |
bh |
2059 |
record its name in self.geometry_column and a quoted version in |
566 |
|
|
self.quoted_geo_col. In any case return the return value of the |
567 |
|
|
inherited method. |
568 |
|
|
""" |
569 |
|
|
col = PostGISTable._create_col_from_description(self, index, |
570 |
|
|
description) |
571 |
|
|
col_name, col_type = description[:2] |
572 |
|
|
if self.geometry_column_was_given: |
573 |
|
|
if (col_name == self.geometry_column |
574 |
|
|
and col_type != self.db.geometry_type): |
575 |
|
|
raise TypeError("Column %s in %s is not a geometry column" |
576 |
|
|
% (self.geometry_column, self.tablename)) |
577 |
|
|
else: |
578 |
|
|
if col is None: |
579 |
|
|
if description[1] == self.db.geometry_type: |
580 |
|
|
# The column is a geometry column. If the name of |
581 |
|
|
# the geometry column was not given to the |
582 |
|
|
# constructor, and we encounter two geometry |
583 |
|
|
# columns, raise a value error |
584 |
|
|
if self.geometry_column is None: |
585 |
|
|
self.geometry_column = description[0] |
586 |
|
|
else: |
587 |
|
|
raise TypeError("Table %s has two geometry columns" |
588 |
|
|
" and no column name was given" |
589 |
|
|
% (self.tablename,)) |
590 |
|
|
return col |
591 |
|
|
|
592 |
bh |
1605 |
def Table(self): |
593 |
|
|
"""Return self since a PostGISShapeStore is its own table.""" |
594 |
|
|
return self |
595 |
|
|
|
596 |
|
|
def OrigShapeStore(self): |
597 |
|
|
"""Return None since the PostGISShapeStore is not derived from another |
598 |
|
|
""" |
599 |
|
|
return None |
600 |
|
|
|
601 |
bh |
2102 |
def GeometryColumn(self): |
602 |
|
|
"""Return the column description object for the geometry column |
603 |
|
|
|
604 |
|
|
There's currently no FIELDTYPE constant for this column, so the |
605 |
|
|
return value is not a regular column object that could also be |
606 |
|
|
returned from e.g. the Column() method. Only the name attribute |
607 |
|
|
of the return value is meaningful at the moment. |
608 |
|
|
""" |
609 |
|
|
return PostGISColumn(self.geometry_column, None, None) |
610 |
|
|
|
611 |
bh |
1605 |
def ShapeType(self): |
612 |
|
|
"""Return the type of the shapes in the shapestore.""" |
613 |
bh |
2106 |
return self.shape_type |
614 |
bh |
1605 |
|
615 |
|
|
def RawShapeFormat(self): |
616 |
|
|
"""Return the raw data format of the shape data. |
617 |
|
|
|
618 |
|
|
For the PostGISShapeStore this is RAW_WKT. |
619 |
|
|
""" |
620 |
|
|
return RAW_WKT |
621 |
|
|
|
622 |
|
|
def NumShapes(self): |
623 |
|
|
# The number of shapes is the same as the number of rows, |
624 |
|
|
# assuming that the geometry can't be NULL. |
625 |
|
|
return self.NumRows() |
626 |
|
|
|
627 |
|
|
def BoundingBox(self): |
628 |
|
|
"""Return the bounding box of all shapes in the postgis table""" |
629 |
|
|
minx = miny = maxx = maxy = None |
630 |
|
|
x=[] |
631 |
|
|
y=[] |
632 |
|
|
cursor = self.db.cursor() |
633 |
|
|
try: |
634 |
bh |
1660 |
# Using the extent function is postgis specific. An OGC |
635 |
|
|
# Simple Features compliant solution would be to use a query |
636 |
|
|
# like "SELECT AsText(Envelope(the_geom)) FROM mytable;" and |
637 |
|
|
# calculate the bounding box by hand from that |
638 |
|
|
cursor.execute("SELECT extent(%s) FROM %s;" |
639 |
bh |
1946 |
% (self.quoted_geo_col, self.quoted_tablename)) |
640 |
bh |
1605 |
result = cursor.fetchone() |
641 |
bh |
2057 |
if result[0]: |
642 |
bh |
1660 |
(minx, miny), (maxx, maxy) \ |
643 |
|
|
= wellknowntext.parse_wkt_thuban(result[0])[0] |
644 |
|
|
return (minx, miny, maxx, maxy) |
645 |
bh |
1605 |
finally: |
646 |
|
|
cursor.close() |
647 |
|
|
|
648 |
|
|
def Shape(self, shapeid): |
649 |
|
|
cursor = self.db.cursor() |
650 |
bh |
2096 |
cursor.execute("SELECT AsText(%s) FROM %s WHERE %s=%d" |
651 |
|
|
% (self.quoted_geo_col, self.quoted_tablename, |
652 |
|
|
self.quoted_id_column, shapeid)) |
653 |
bh |
1605 |
wkt = cursor.fetchone()[0] |
654 |
|
|
cursor.close() |
655 |
|
|
return PostGISShape(shapeid, wkt) |
656 |
|
|
|
657 |
bh |
1658 |
def AllShapes(self): |
658 |
|
|
cursor = self.db.cursor() |
659 |
bh |
2096 |
cursor.execute("SELECT %s, AsText(%s) FROM %s ORDER BY %s" |
660 |
|
|
% (self.quoted_id_column, self.quoted_geo_col, |
661 |
|
|
self.quoted_tablename, self.quoted_id_column)) |
662 |
bh |
1658 |
while 1: |
663 |
|
|
result = cursor.fetchone() |
664 |
|
|
if result is None: |
665 |
|
|
return |
666 |
|
|
yield PostGISShape(result[0], result[1]) |
667 |
|
|
|
668 |
|
|
|
669 |
bh |
1605 |
def ShapesInRegion(self, bbox): |
670 |
|
|
"""Generate all shapes overlapping the region given by bbox.""" |
671 |
|
|
# IMPORTANT:This will work for PostGIS < 0.8 |
672 |
|
|
left, bottom, right, top = bbox |
673 |
|
|
geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))") |
674 |
|
|
% (left, bottom, left, top, right, top, right, bottom, |
675 |
|
|
left, bottom)) |
676 |
|
|
cursor = self.db.cursor() |
677 |
bh |
2096 |
cursor.execute("SELECT %(gid)s, AsText(%(geom)s) FROM %(table)s" |
678 |
|
|
" WHERE %(geom)s && GeometryFromText('%(box)s', %(srid)d)" |
679 |
|
|
" ORDER BY %(gid)s" |
680 |
|
|
% {"table": self.quoted_tablename, |
681 |
|
|
"geom": self.quoted_geo_col, |
682 |
|
|
"gid": self.quoted_id_column, |
683 |
|
|
"box": geom, |
684 |
|
|
"srid": self.srid}) |
685 |
bh |
1605 |
while 1: |
686 |
|
|
result = cursor.fetchone() |
687 |
|
|
if result is None: |
688 |
bh |
1658 |
return |
689 |
bh |
1605 |
yield PostGISShape(result[0], result[1]) |