/[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 1662 - (show annotations)
Wed Aug 27 13:51:01 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: 13330 byte(s)
Make the table interface distinguish between row ids (an integer
that uniquely identifies a row) and row ordinals (a simple row
count from 0 to NumRows() - 1)

* Thuban/Model/postgisdb.py (PostGISTable.RowIdToOrdinal)
(PostGISTable.RowOrdinalToId): New methods to conver between row
ids and row ordinals
(PostGISTable.ReadRowAsDict, PostGISTable.ReadValue): New keyword
parameter row_is_ordinal to indicate whether the row parameter is
the row id or the ordinal

* Thuban/Model/transientdb.py (TransientTableBase.RowIdToOrdinal)
(TransientTableBase.RowOrdinalToId)
(AutoTransientTable.RowIdToOrdinal)
(AutoTransientTable.RowOrdinalToId): Same new methods as in
PostGISTable.
(TransientTableBase.ReadRowAsDict, TransientTableBase.ReadValue)
(AutoTransientTable.ReadRowAsDict, AutoTransientTable.ReadValue):
Same new parameter as in PostGISTable.

* Thuban/Model/table.py (DBFTable.RowIdToOrdinal)
(DBFTable.RowOrdinalToId, MemoryTable.RowIdToOrdinal)
(MemoryTable.RowOrdinalToId): Same new methods as in PostGISTable.
(DBFTable.ReadValue, DBFTable.ReadRowAsDict)
(MemoryTable.ReadValue, MemoryTable.ReadRowAsDict): Same new
parameter as in PostGISTable.

* Thuban/UI/tableview.py (DataTable.RowIdToOrdinal)
(DataTable.RowOrdinalToId): New methods to convert between row ids
and row ordinals.
(TableGrid.SelectRowById): New method to select a row based on its
ID as opposed to its ordinal
(DataTable.GetValue, TableGrid.OnRangeSelect)
(TableGrid.OnSelectCell, LayerTableGrid.select_shapes)
(QueryTableFrame.OnQuery, QueryTableFrame.get_selected)
(LayerTableFrame.__init__): Convert between row ids and row
ordinals as appropriate

* test/postgissupport.py (PostGISDatabase.__init__): Add
doc-string.
(PostGISDatabase.initdb): The optional third item in a tuple in
tables is now a (key, value) list with additional arguments to
pass to upload_shapefile
(upload_shapefile): New parameter gid_offset to allow gids that
are not the same as the shapeids in the shapefile
(PostgreSQLServer.get_default_static_data_db): Use the new
gid_offset to make the gids in landmarks 1000 higher than the
shapeids in the shapefile

* test/test_viewport.py
(TestViewportWithPostGIS.test_find_shape_at_point): Adapt to the
new shapeids in the landmarks table

* test/test_transientdb.py
(TestTransientTable.run_iceland_political_tests)
(TestTransientTable.test_transient_joined_table): Add tests for
the new table methods and new keywords arguments.

* test/test_postgis_db.py
(TestPostGISTable.test_read_row_as_dict_row_count_mode)
(TestPostGISTable.test_read_value_row_count_mode)
(TestPostGISTable.test_row_id_to_ordinal)
(TestPostGISTable.test_row_oridnal_to_id): New test for the new
table methods and the new arguments
(TestPostGISShapestorePoint.test_shapes_in_region)
(TestPostGISShapestorePoint.test_shape_raw_data)
(TestPostGISShapestorePoint.test_shape_points)
(TestPostGISShapestorePoint.test_shape_shapeid)
(TestPostGISShapestorePoint.test_all_shapes)
(TestPostGISTable.test_simple_query)
(TestPostGISTable.test_simple_query)
(TestPostGISTable.test_simple_query)
(TestPostGISTable.test_read_value)
(TestPostGISTable.test_read_row_as_dict): Adapt to the new
shapeids in the landmarks table

* test/test_memory_table.py
(TestMemoryTable.test_read_row_as_dict_row_count_mode)
(TestMemoryTable.test_read_value_row_count_mode)
(TestMemoryTable.test_row_id_to_ordinal)
(TestMemoryTable.test_row_oridnal_to_id): New test for the new
table methods and the new arguments

* test/test_dbf_table.py
(TestDBFTable.test_read_row_as_dict_row_count_mode)
(TestDBFTable.test_read_value_row_count_mode)
(TestDBFTable.test_row_id_to_ordinal)
(TestDBFTable.test_row_oridnal_to_id): New test for the new table
methods and the new arguments

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 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 = [(psycopg.STRING, table.FIELDTYPE_STRING),
36 (psycopg.INTEGER, table.FIELDTYPE_INT),
37 (psycopg.FLOAT, table.FIELDTYPE_DOUBLE)]
38
39
40 class ConnectionError(Exception):
41
42 """Class for exceptions occurring when establishing a Databse connection"""
43
44
45 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 self.connect()
68
69 def connect(self):
70 """Internal: Establish the database connection"""
71 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 try:
77 self.connection = psycopg.connect(" ".join(params))
78 except psycopg.OperationalError, val:
79 raise ConnectionError(str(val))
80
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 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 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 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 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 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 def RowIdToOrdinal(self, gid):
213 """Return the row ordinal given its id"""
214 cursor = self.db.cursor()
215 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 result = {}
234 for col, value in zip(self.columns, cursor.fetchone()):
235 result[col.name] = value
236 return result
237
238 def ReadValue(self, row, col, row_is_ordinal = 0):
239 cursor = self.db.cursor()
240 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 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 "MULTIPOLYGON": SHAPETYPE_POLYGON,
321 "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 # 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 result = cursor.fetchone()
373 if result:
374 (minx, miny), (maxx, maxy) \
375 = wellknowntext.parse_wkt_thuban(result[0])[0]
376 return (minx, miny, maxx, maxy)
377 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 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 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 return
415 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