/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/Model/load.py
ViewVC logotype

Contents of /branches/WIP-pyshapelib-bramz/Thuban/Model/load.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1646 - (show annotations)
Mon Aug 25 13:43:56 2003 UTC (21 years, 6 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/load.py
File MIME type: text/x-python
File size: 21971 byte(s)
Basic loading of sessions containing postgis connections:

* Thuban/Model/load.py (LoadError): Add doc-string
(LoadCancelled): New exception class to indicate a cancelled load
(SessionLoader.__init__): Add the db_connection_callback parameter
which will be used by the loader to get updated parameters and a
password for a database connection
(SessionLoader.__init__): Add the new XML elements to the
dispatchers dictionary
(SessionLoader.check_attrs): Two new conversions, ascii to convert
to a byte-string object and idref as a generic id reference
(SessionLoader.start_dbconnection)
(SessionLoader.start_dbshapesource): New. Handlers for the new XML
elements
(load_session): Add the db_connection_callback to pass through the
SessionLoader

* test/test_load.py (TestPostGISLayer, TestPostGISLayerPassword):
New classes to test loading of sessions with postgis database
connections.

1 # Copyright (C) 2001, 2002, 2003 by Intevation GmbH
2 # Authors:
3 # Jan-Oliver Wagner <[email protected]>
4 # Bernhard Herzog <[email protected]>
5 # Jonathan Coles <[email protected]>
6 #
7 # This program is free software under the GPL (>=v2)
8 # Read the file COPYING coming with GRASS for details.
9
10 """
11 Parser for thuban session files.
12 """
13
14 __version__ = "$Revision$"
15
16 import string, os
17
18 import xml.sax
19 import xml.sax.handler
20 from xml.sax import make_parser, ErrorHandler, SAXNotRecognizedException
21
22 from Thuban import _
23
24 from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
25 FIELDTYPE_STRING
26
27 from Thuban.Model.color import Color, Transparent
28
29 from Thuban.Model.session import Session
30 from Thuban.Model.map import Map
31 from Thuban.Model.layer import Layer, RasterLayer
32 from Thuban.Model.proj import Projection
33 from Thuban.Model.range import Range
34 from Thuban.Model.classification import Classification, \
35 ClassGroupDefault, ClassGroupSingleton, ClassGroupRange, ClassGroupMap, \
36 ClassGroupProperties
37 from Thuban.Model.data import DerivedShapeStore, ShapefileStore
38 from Thuban.Model.table import DBFTable
39 from Thuban.Model.transientdb import TransientJoinedTable
40
41 from Thuban.Model.xmlreader import XMLReader
42 import resource
43
44 import postgisdb
45
46 class LoadError(Exception):
47
48 """Exception raised when the thuban file is corrupted
49
50 Not all cases of corrupted thuban files will lead to this exception
51 but those that are found by checks in the loading code itself are.
52 """
53
54
55 class LoadCancelled(Exception):
56
57 """Exception raised to indicate that loading was interrupted by the user"""
58
59
60 def parse_color(color):
61 """Return the color object for the string color.
62
63 Color may be either 'None' or of the form '#RRGGBB' in the usual
64 HTML color notation
65 """
66 color = string.strip(color)
67 if color == "None":
68 result = Transparent
69 elif color[0] == '#':
70 if len(color) == 7:
71 r = string.atoi(color[1:3], 16) / 255.0
72 g = string.atoi(color[3:5], 16) / 255.0
73 b = string.atoi(color[5:7], 16) / 255.0
74 result = Color(r, g, b)
75 else:
76 raise ValueError(_("Invalid hexadecimal color specification %s")
77 % color)
78 else:
79 raise ValueError(_("Invalid color specification %s") % color)
80 return result
81
82 class AttrDesc:
83
84 def __init__(self, name, required = False, default = "",
85 conversion = None):
86 if not isinstance(name, tuple):
87 fullname = (None, name)
88 else:
89 fullname = name
90 name = name[1]
91 self.name = name
92 self.fullname = fullname
93 self.required = required
94 self.default = default
95 self.conversion = conversion
96
97 # set by the SessionLoader's check_attrs method
98 self.value = None
99
100
101 class SessionLoader(XMLReader):
102
103 def __init__(self, db_connection_callback = None):
104 """Inititialize the Sax handler."""
105 XMLReader.__init__(self)
106
107 self.db_connection_callback = db_connection_callback
108
109 self.theSession = None
110 self.aMap = None
111 self.aLayer = None
112
113 # Map ids used in the thuban file to the corresponding objects
114 # in the session
115 self.idmap = {}
116
117 dispatchers = {
118 'session' : ("start_session", "end_session"),
119
120 'dbconnection': ("start_dbconnection", None),
121
122 'dbshapesource': ("start_dbshapesource", None),
123 'fileshapesource': ("start_fileshapesource", None),
124 'derivedshapesource': ("start_derivedshapesource", None),
125 'filetable': ("start_filetable", None),
126 'jointable': ("start_jointable", None),
127
128 'map' : ("start_map", "end_map"),
129 'projection' : ("start_projection", "end_projection"),
130 'parameter' : ("start_parameter", None),
131 'layer' : ("start_layer", "end_layer"),
132 'rasterlayer' : ("start_rasterlayer", "end_rasterlayer"),
133 'classification': ("start_classification", "end_classification"),
134 'clnull' : ("start_clnull", "end_clnull"),
135 'clpoint' : ("start_clpoint", "end_clpoint"),
136 'clrange' : ("start_clrange", "end_clrange"),
137 'cldata' : ("start_cldata", "end_cldata"),
138 'table' : ("start_table", "end_table"),
139 'labellayer' : ("start_labellayer", None),
140 'label' : ("start_label", None)}
141
142 # all dispatchers should be used for the 0.8 and 0.9 namespaces too
143 for xmlns in ("http://thuban.intevation.org/dtds/thuban-0.8.dtd",
144 "http://thuban.intevation.org/dtds/thuban-0.9-dev.dtd"):
145 for key, value in dispatchers.items():
146 dispatchers[(xmlns, key)] = value
147
148 XMLReader.AddDispatchers(self, dispatchers)
149
150 def start_session(self, name, qname, attrs):
151 self.theSession = Session(self.encode(attrs.get((None, 'title'),
152 None)))
153
154 def end_session(self, name, qname):
155 pass
156
157 def check_attrs(self, element, attrs, descr):
158 """Check and convert some of the attributes of an element
159
160 Parameters:
161 element -- The element name
162 attrs -- The attrs mapping as passed to the start_* methods
163 descr -- Sequence of attribute descriptions (AttrDesc instances)
164
165 Return a dictionary containig normalized versions of the
166 attributes described in descr. The keys of that dictionary are
167 the name attributes of the attribute descriptions. The attrs
168 dictionary will not be modified.
169
170 If the attribute is required, i.e. the 'required' attribute of
171 the descrtiption is true, but it is not in attrs, raise a
172 LoadError.
173
174 If the attribute has a default value and it is not present in
175 attrs, use that default value as the value in the returned dict.
176
177 If a conversion is specified, convert the value before putting
178 it into the returned dict. The following conversions are
179 available:
180
181 'filename' -- The attribute is a filename.
182
183 If the filename is a relative name, interpret
184 it relative to the directory containing the
185 .thuban file and make it an absolute name
186
187 'shapestore' -- The attribute is the ID of a shapestore
188 defined earlier in the .thuban file. Look it
189 up self.idmap
190
191 'table' -- The attribute is the ID of a table or shapestore
192 defined earlier in the .thuban file. Look it up
193 self.idmap. If it's the ID of a shapestore the
194 value will be the table of the shapestore.
195
196 'idref' -- The attribute is the id of an object defined
197 earlier in the .thuban file. Look it up self.idmap
198
199 'ascii' -- The attribute is converted to a bytestring with
200 ascii encoding.
201 """
202 normalized = {}
203
204 for d in descr:
205 if d.required and not attrs.has_key(d.fullname):
206 raise LoadError("Element %s requires an attribute %r"
207 % (element, d.name))
208 value = attrs.get(d.fullname, d.default)
209
210 if d.conversion in ("idref", "shapesource"):
211 if value in self.idmap:
212 value = self.idmap[value]
213 else:
214 raise LoadError("Element %s requires an already defined ID"
215 " in attribute %r"
216 % (element, d.name))
217 elif d.conversion == "table":
218 if value in self.idmap:
219 value = self.idmap[value]
220 if isinstance(value, ShapefileStore):
221 value = value.Table()
222 else:
223 raise LoadError("Element %s requires an already defined ID"
224 " in attribute %r"
225 % (element, d.name))
226 elif d.conversion == "filename":
227 value = os.path.abspath(os.path.join(self.GetDirectory(),
228 value))
229 elif d.conversion == "ascii":
230 value = value.encode("ascii")
231 else:
232 if d.conversion:
233 raise ValueError("Unknown attribute conversion %r"
234 % d.conversion)
235
236 normalized[d.name] = value
237 return normalized
238
239 def start_dbconnection(self, name, qname, attrs):
240 attrs = self.check_attrs(name, attrs,
241 [AttrDesc("id", True),
242 AttrDesc("dbtype", True),
243 AttrDesc("host", False, ""),
244 AttrDesc("port", False, ""),
245 AttrDesc("user", False, ""),
246 AttrDesc("dbname", True)])
247 ID = attrs["id"]
248 dbtype = attrs["dbtype"]
249 if dbtype != "postgis":
250 raise LoadError("dbtype %r not supported" % filetype)
251
252 del attrs["id"]
253 del attrs["dbtype"]
254
255 # Try to open the connection and if it fails ask the user for
256 # the correct parameters repeatedly.
257 # FIXME: it would be better not to insist on getting a
258 # connection here. We should handle this more like the raster
259 # images where the layers etc still are created but are not
260 # drawn in case Thuban can't use the data for various reasons
261 while 1:
262 try:
263 conn = postgisdb.PostGISConnection(**attrs)
264 break
265 except postgisdb.ConnectionError, val:
266 if self.db_connection_callback is not None:
267 attrs = self.db_connection_callback(attrs, str(val))
268 if attrs is None:
269 raise LoadCancelled
270 else:
271 raise
272
273 self.idmap[ID] = conn
274 self.theSession.AddDBConnection(conn)
275
276 def start_dbshapesource(self, name, qname, attrs):
277 attrs = self.check_attrs(name, attrs,
278 [AttrDesc("id", True),
279 AttrDesc("dbconn", True,
280 conversion = "idref"),
281 AttrDesc("tablename", True,
282 conversion = "ascii")])
283 ID = attrs["id"]
284 db = attrs["dbconn"]
285 tablename = attrs["tablename"]
286 self.idmap[ID] = self.theSession.OpenDBShapeStore(db, tablename)
287
288 def start_fileshapesource(self, name, qname, attrs):
289 attrs = self.check_attrs(name, attrs,
290 [AttrDesc("id", True),
291 AttrDesc("filename", True,
292 conversion = "filename"),
293 AttrDesc("filetype", True)])
294 ID = attrs["id"]
295 filename = attrs["filename"]
296 filetype = attrs["filetype"]
297 if filetype != "shapefile":
298 raise LoadError("shapesource filetype %r not supported" % filetype)
299 self.idmap[ID] = self.theSession.OpenShapefile(filename)
300
301 def start_derivedshapesource(self, name, qname, attrs):
302 attrs = self.check_attrs(name, attrs,
303 [AttrDesc("id", True),
304 AttrDesc("shapesource", True,
305 conversion = "shapesource"),
306 AttrDesc("table", True, conversion="table")])
307 store = DerivedShapeStore(attrs["shapesource"], attrs["table"])
308 self.theSession.AddShapeStore(store)
309 self.idmap[attrs["id"]] = store
310
311 def start_filetable(self, name, qname, attrs):
312 attrs = self.check_attrs(name, attrs,
313 [AttrDesc("id", True),
314 AttrDesc("title", True),
315 AttrDesc("filename", True,
316 conversion = "filename"),
317 AttrDesc("filetype")])
318 filetype = attrs["filetype"]
319 if filetype != "DBF":
320 raise LoadError("shapesource filetype %r not supported" % filetype)
321 table = DBFTable(attrs["filename"])
322 table.SetTitle(attrs["title"])
323 self.idmap[attrs["id"]] = self.theSession.AddTable(table)
324
325 def start_jointable(self, name, qname, attrs):
326 attrs = self.check_attrs(name, attrs,
327 [AttrDesc("id", True),
328 AttrDesc("title", True),
329 AttrDesc("left", True, conversion="table"),
330 AttrDesc("leftcolumn", True),
331 AttrDesc("right", True, conversion="table"),
332 AttrDesc("rightcolumn", True),
333
334 # jointype is required for file
335 # version 0.9 but this attribute
336 # wasn't in the 0.8 version because of
337 # an oversight so we assume it's
338 # optional since we want to handle
339 # both file format versions here.
340 AttrDesc("jointype", False,
341 default="INNER")])
342
343 jointype = attrs["jointype"]
344 if jointype == "LEFT OUTER":
345 outer_join = True
346 elif jointype == "INNER":
347 outer_join = False
348 else:
349 raise LoadError("jointype %r not supported" % jointype )
350 table = TransientJoinedTable(self.theSession.TransientDB(),
351 attrs["left"], attrs["leftcolumn"],
352 attrs["right"], attrs["rightcolumn"],
353 outer_join = outer_join)
354 table.SetTitle(attrs["title"])
355 self.idmap[attrs["id"]] = self.theSession.AddTable(table)
356
357 def start_map(self, name, qname, attrs):
358 """Start a map."""
359 self.aMap = Map(self.encode(attrs.get((None, 'title'), None)))
360
361 def end_map(self, name, qname):
362 self.theSession.AddMap(self.aMap)
363 self.aMap = None
364
365 def start_projection(self, name, qname, attrs):
366 self.ProjectionName = self.encode(attrs.get((None, 'name'), None))
367 self.ProjectionParams = [ ]
368
369 def end_projection(self, name, qname):
370 if self.aLayer is not None:
371 obj = self.aLayer
372 elif self.aMap is not None:
373 obj = self.aMap
374 else:
375 assert False, "projection tag out of context"
376 pass
377
378 obj.SetProjection(
379 Projection(self.ProjectionParams, self.ProjectionName))
380
381 def start_parameter(self, name, qname, attrs):
382 s = attrs.get((None, 'value'))
383 s = str(s) # we can't handle unicode in proj
384 self.ProjectionParams.append(s)
385
386 def start_layer(self, name, qname, attrs, layer_class = Layer):
387 """Start a layer
388
389 Instantiate a layer of class layer_class from the attributes in
390 attrs which may be a dictionary as well as the normal SAX attrs
391 object and bind it to self.aLayer.
392 """
393 title = self.encode(attrs.get((None, 'title'), ""))
394 filename = attrs.get((None, 'filename'), "")
395 filename = os.path.join(self.GetDirectory(), filename)
396 filename = self.encode(filename)
397 visible = self.encode(attrs.get((None, 'visible'), "true")) != "false"
398 fill = parse_color(attrs.get((None, 'fill'), "None"))
399 stroke = parse_color(attrs.get((None, 'stroke'), "#000000"))
400 stroke_width = int(attrs.get((None, 'stroke_width'), "1"))
401 if attrs.has_key((None, "shapestore")):
402 store = self.idmap[attrs[(None, "shapestore")]]
403 else:
404 store = self.theSession.OpenShapefile(filename)
405 self.aLayer = layer_class(title, store,
406 fill = fill, stroke = stroke,
407 lineWidth = stroke_width,
408 visible = visible)
409
410 def end_layer(self, name, qname):
411 self.aMap.AddLayer(self.aLayer)
412 self.aLayer = None
413
414 def start_rasterlayer(self, name, qname, attrs, layer_class = RasterLayer):
415 title = self.encode(attrs.get((None, 'title'), ""))
416 filename = attrs.get((None, 'filename'), "")
417 filename = os.path.join(self.GetDirectory(), filename)
418 filename = self.encode(filename)
419 visible = self.encode(attrs.get((None, 'visible'), "true")) != "false"
420
421 self.aLayer = layer_class(title, filename, visible = visible)
422
423 def end_rasterlayer(self, name, qname):
424 self.aMap.AddLayer(self.aLayer)
425 self.aLayer = None
426
427 def start_classification(self, name, qname, attrs):
428 field = attrs.get((None, 'field'), None)
429
430 fieldType = attrs.get((None, 'field_type'), None)
431 dbFieldType = self.aLayer.GetFieldType(field)
432
433 if fieldType != dbFieldType:
434 raise ValueError(_("xml field type differs from database!"))
435
436 # setup conversion routines depending on the kind of data
437 # we will be seeing later on
438 if fieldType == FIELDTYPE_STRING:
439 self.conv = str
440 elif fieldType == FIELDTYPE_INT:
441 self.conv = lambda p: int(float(p))
442 elif fieldType == FIELDTYPE_DOUBLE:
443 self.conv = float
444
445 self.aLayer.SetClassificationColumn(field)
446
447 def end_classification(self, name, qname):
448 pass
449
450 def start_clnull(self, name, qname, attrs):
451 self.cl_group = ClassGroupDefault()
452 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
453 self.cl_prop = ClassGroupProperties()
454
455 def end_clnull(self, name, qname):
456 self.cl_group.SetProperties(self.cl_prop)
457 self.aLayer.GetClassification().SetDefaultGroup(self.cl_group)
458 del self.cl_group, self.cl_prop
459
460 def start_clpoint(self, name, qname, attrs):
461 attrib_value = attrs.get((None, 'value'), "0")
462
463 field = self.aLayer.GetClassificationColumn()
464 if self.aLayer.GetFieldType(field) == FIELDTYPE_STRING:
465 value = self.encode(attrib_value)
466 else:
467 value = self.conv(attrib_value)
468 self.cl_group = ClassGroupSingleton(value)
469 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
470 self.cl_prop = ClassGroupProperties()
471
472
473 def end_clpoint(self, name, qname):
474 self.cl_group.SetProperties(self.cl_prop)
475 self.aLayer.GetClassification().AppendGroup(self.cl_group)
476 del self.cl_group, self.cl_prop
477
478 def start_clrange(self, name, qname, attrs):
479
480 range = attrs.get((None, 'range'), None)
481 # for backward compatibility (min/max are not saved)
482 min = attrs.get((None, 'min'), None)
483 max = attrs.get((None, 'max'), None)
484
485 try:
486 if range is not None:
487 self.cl_group = ClassGroupRange(Range(range))
488 elif min is not None and max is not None:
489 self.cl_group = ClassGroupRange((self.conv(min),
490 self.conv(max)))
491 else:
492 self.cl_group = ClassGroupRange(Range(None))
493
494 except ValueError:
495 raise ValueError(_("Classification range is not a number!"))
496
497 self.cl_group.SetLabel(attrs.get((None, 'label'), ""))
498 self.cl_prop = ClassGroupProperties()
499
500
501 def end_clrange(self, name, qname):
502 self.cl_group.SetProperties(self.cl_prop)
503 self.aLayer.GetClassification().AppendGroup(self.cl_group)
504 del self.cl_group, self.cl_prop
505
506 def start_cldata(self, name, qname, attrs):
507 self.cl_prop.SetLineColor(
508 parse_color(attrs.get((None, 'stroke'), "None")))
509 self.cl_prop.SetLineWidth(
510 int(attrs.get((None, 'stroke_width'), "0")))
511 self.cl_prop.SetFill(parse_color(attrs.get((None, 'fill'), "None")))
512
513 def end_cldata(self, name, qname):
514 pass
515
516 def start_labellayer(self, name, qname, attrs):
517 self.aLayer = self.aMap.LabelLayer()
518
519 def start_label(self, name, qname, attrs):
520 x = float(attrs[(None, 'x')])
521 y = float(attrs[(None, 'y')])
522 text = self.encode(attrs[(None, 'text')])
523 halign = attrs[(None, 'halign')]
524 valign = attrs[(None, 'valign')]
525 self.aLayer.AddLabel(x, y, text, halign = halign, valign = valign)
526
527 def characters(self, chars):
528 pass
529
530
531 def load_session(filename, db_connection_callback = None):
532 """Load a Thuban session from the file object file
533
534 The db_connection_callback, if given should be a callable object
535 that can be called like this:
536 db_connection_callback(params, message)
537
538 where params is a dictionary containing the known connection
539 parameters and message is a string with a message why the connection
540 failed. db_connection_callback should return a new dictionary with
541 corrected and perhaps additional parameters like a password or None
542 to indicate that the user cancelled.
543 """
544 handler = SessionLoader(db_connection_callback)
545 handler.read(filename)
546
547 session = handler.theSession
548 # Newly loaded session aren't modified
549 session.UnsetModified()
550
551 return session
552

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26