/[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 2004 - (show annotations)
Tue Dec 2 13:25:55 2003 UTC (21 years, 3 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/load.py
File MIME type: text/x-python
File size: 23317 byte(s)
* Thuban/Model/save.py (SessionSaver.write_session): Save files
with the thuban-1.0rc1

* Thuban/Model/load.py (SessionLoader.__init__): Recognize the
thuban-1.0rc1 namespace too

* test/test_save.py (SaveSessionTest.dtd)
(SaveSessionTest.testEmptySession)
(SaveSessionTest.testSingleLayer)
(SaveSessionTest.testLayerProjection)
(SaveSessionTest.testRasterLayer)
(SaveSessionTest.testClassifiedLayer)
(SaveSessionTest.test_dbf_table)
(SaveSessionTest.test_joined_table)
(SaveSessionTest.test_save_postgis): Update to thuban-1.0rc1
namespace

* test/test_load.py (LoadSessionTest.dtd): Update to thuban-1.0rc1
namespace
(TestSingleLayer.file_contents)
(TestNonAsciiColumnName.file_contents)
(TestLayerVisibility.file_contents)
(TestClassification.file_contents, TestLabels.file_contents)
(TestLayerProjection.file_contents)
(TestRasterLayer.file_contents, TestJoinedTable.file_contents)
(TestPostGISLayer.file_contents)
(TestPostGISLayerPassword.file_contents)
(TestLoadError.file_contents, TestLoadError.test): Update to
thuban-1.0rc1 namespace

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 "http://thuban.intevation.org/dtds/thuban-0.9.dtd",
146 "http://thuban.intevation.org/dtds/thuban-1.0-dev.dtd",
147 "http://thuban.intevation.org/dtds/thuban-1.0rc1.dtd"):
148 for key, value in dispatchers.items():
149 dispatchers[(xmlns, key)] = value
150
151 XMLReader.AddDispatchers(self, dispatchers)
152
153 def Destroy(self):
154 """Clear all instance variables to cut cyclic references.
155
156 The GC would have collected the loader eventually but it can
157 happen that it doesn't run at all until Thuban is closed (2.3
158 but not 2.2 tries a bit harder and forces a collection when the
159 interpreter terminates)
160 """
161 self.__dict__.clear()
162
163 def start_session(self, name, qname, attrs):
164 self.theSession = Session(self.encode(attrs.get((None, 'title'),
165 None)))
166
167 def end_session(self, name, qname):
168 pass
169
170 def check_attrs(self, element, attrs, descr):
171 """Check and convert some of the attributes of an element
172
173 Parameters:
174 element -- The element name
175 attrs -- The attrs mapping as passed to the start_* methods
176 descr -- Sequence of attribute descriptions (AttrDesc instances)
177
178 Return a dictionary containig normalized versions of the
179 attributes described in descr. The keys of that dictionary are
180 the name attributes of the attribute descriptions. The attrs
181 dictionary will not be modified.
182
183 If the attribute is required, i.e. the 'required' attribute of
184 the descrtiption is true, but it is not in attrs, raise a
185 LoadError.
186
187 If the attribute has a default value and it is not present in
188 attrs, use that default value as the value in the returned dict.
189
190 The value is converted before putting it into the returned dict.
191 The following conversions are available:
192
193 'filename' -- The attribute is a filename.
194
195 If the filename is a relative name, interpret
196 it relative to the directory containing the
197 .thuban file and make it an absolute name
198
199 'shapestore' -- The attribute is the ID of a shapestore
200 defined earlier in the .thuban file. Look it
201 up self.idmap
202
203 'table' -- The attribute is the ID of a table or shapestore
204 defined earlier in the .thuban file. Look it up
205 self.idmap. If it's the ID of a shapestore the
206 value will be the table of the shapestore.
207
208 'idref' -- The attribute is the id of an object defined
209 earlier in the .thuban file. Look it up self.idmap
210
211 'ascii' -- The attribute is converted to a bytestring with
212 ascii encoding.
213
214 a callable -- The attribute value is passed to the callable
215 and the return value is used as the converted
216 value
217
218 If no conversion is specified for an attribute it is converted
219 with self.encode.
220 """
221 normalized = {}
222
223 for d in descr:
224 if d.required and not attrs.has_key(d.fullname):
225 raise LoadError("Element %s requires an attribute %r"
226 % (element, d.name))
227 value = attrs.get(d.fullname, d.default)
228
229 if d.conversion in ("idref", "shapesource"):
230 if value in self.idmap:
231 value = self.idmap[value]
232 else:
233 raise LoadError("Element %s requires an already defined ID"
234 " in attribute %r"
235 % (element, d.name))
236 elif d.conversion == "table":
237 if value in self.idmap:
238 value = self.idmap[value]
239 if isinstance(value, ShapefileStore):
240 value = value.Table()
241 else:
242 raise LoadError("Element %s requires an already defined ID"
243 " in attribute %r"
244 % (element, d.name))
245 elif d.conversion == "filename":
246 value = os.path.abspath(os.path.join(self.GetDirectory(),
247 value))
248 elif d.conversion == "ascii":
249 value = value.encode("ascii")
250 elif d.conversion:
251 # Assume it's a callable
252 value = d.conversion(value)
253 else:
254 value = self.encode(value)
255
256 normalized[d.name] = value
257 return normalized
258
259 def start_dbconnection(self, name, qname, attrs):
260 attrs = self.check_attrs(name, attrs,
261 [AttrDesc("id", True),
262 AttrDesc("dbtype", True),
263 AttrDesc("host", False, ""),
264 AttrDesc("port", False, ""),
265 AttrDesc("user", False, ""),
266 AttrDesc("dbname", True)])
267 ID = attrs["id"]
268 dbtype = attrs["dbtype"]
269 if dbtype != "postgis":
270 raise LoadError("dbtype %r not supported" % filetype)
271
272 del attrs["id"]
273 del attrs["dbtype"]
274
275 # Try to open the connection and if it fails ask the user for
276 # the correct parameters repeatedly.
277 # FIXME: it would be better not to insist on getting a
278 # connection here. We should handle this more like the raster
279 # images where the layers etc still are created but are not
280 # drawn in case Thuban can't use the data for various reasons
281 while 1:
282 try:
283 conn = postgisdb.PostGISConnection(**attrs)
284 break
285 except postgisdb.ConnectionError, val:
286 if self.db_connection_callback is not None:
287 attrs = self.db_connection_callback(attrs, str(val))
288 if attrs is None:
289 raise LoadCancelled
290 else:
291 raise
292
293 self.idmap[ID] = conn
294 self.theSession.AddDBConnection(conn)
295
296 def start_dbshapesource(self, name, qname, attrs):
297 attrs = self.check_attrs(name, attrs,
298 [AttrDesc("id", True),
299 AttrDesc("dbconn", True,
300 conversion = "idref"),
301 AttrDesc("tablename", True,
302 conversion = "ascii")])
303 ID = attrs["id"]
304 db = attrs["dbconn"]
305 tablename = attrs["tablename"]
306 self.idmap[ID] = self.theSession.OpenDBShapeStore(db, tablename)
307
308 def start_fileshapesource(self, name, qname, attrs):
309 attrs = self.check_attrs(name, attrs,
310 [AttrDesc("id", True),
311 AttrDesc("filename", True,
312 conversion = "filename"),
313 AttrDesc("filetype", True)])
314 ID = attrs["id"]
315 filename = attrs["filename"]
316 filetype = attrs["filetype"]
317 if filetype != "shapefile":
318 raise LoadError("shapesource filetype %r not supported" % filetype)
319 self.idmap[ID] = self.theSession.OpenShapefile(filename)
320
321 def start_derivedshapesource(self, name, qname, attrs):
322 attrs = self.check_attrs(name, attrs,
323 [AttrDesc("id", True),
324 AttrDesc("shapesource", True,
325 conversion = "shapesource"),
326 AttrDesc("table", True, conversion="table")])
327 store = DerivedShapeStore(attrs["shapesource"], attrs["table"])
328 self.theSession.AddShapeStore(store)
329 self.idmap[attrs["id"]] = store
330
331 def start_filetable(self, name, qname, attrs):
332 attrs = self.check_attrs(name, attrs,
333 [AttrDesc("id", True),
334 AttrDesc("title", True),
335 AttrDesc("filename", True,
336 conversion = "filename"),
337 AttrDesc("filetype")])
338 filetype = attrs["filetype"]
339 if filetype != "DBF":
340 raise LoadError("shapesource filetype %r not supported" % filetype)
341 table = DBFTable(attrs["filename"])
342 table.SetTitle(attrs["title"])
343 self.idmap[attrs["id"]] = self.theSession.AddTable(table)
344
345 def start_jointable(self, name, qname, attrs):
346 attrs = self.check_attrs(name, attrs,
347 [AttrDesc("id", True),
348 AttrDesc("title", True),
349 AttrDesc("left", True, conversion="table"),
350 AttrDesc("leftcolumn", True),
351 AttrDesc("right", True, conversion="table"),
352 AttrDesc("rightcolumn", True),
353
354 # jointype is required for file
355 # version 0.9 but this attribute
356 # wasn't in the 0.8 version because of
357 # an oversight so we assume it's
358 # optional since we want to handle
359 # both file format versions here.
360 AttrDesc("jointype", False,
361 default="INNER")])
362
363 jointype = attrs["jointype"]
364 if jointype == "LEFT OUTER":
365 outer_join = True
366 elif jointype == "INNER":
367 outer_join = False
368 else:
369 raise LoadError("jointype %r not supported" % jointype )
370 table = TransientJoinedTable(self.theSession.TransientDB(),
371 attrs["left"], attrs["leftcolumn"],
372 attrs["right"], attrs["rightcolumn"],
373 outer_join = outer_join)
374 table.SetTitle(attrs["title"])
375 self.idmap[attrs["id"]] = self.theSession.AddTable(table)
376
377 def start_map(self, name, qname, attrs):
378 """Start a map."""
379 self.aMap = Map(self.encode(attrs.get((None, 'title'), None)))
380
381 def end_map(self, name, qname):
382 self.theSession.AddMap(self.aMap)
383 self.aMap = None
384
385 def start_projection(self, name, qname, attrs):
386 attrs = self.check_attrs(name, attrs,
387 [AttrDesc("name", conversion=self.encode),
388 AttrDesc("epsg", default=None,
389 conversion=self.encode)])
390 self.projection_name = attrs["name"]
391 self.projection_epsg = attrs["epsg"]
392 self.projection_params = [ ]
393
394 def end_projection(self, name, qname):
395 if self.aLayer is not None:
396 obj = self.aLayer
397 elif self.aMap is not None:
398 obj = self.aMap
399 else:
400 assert False, "projection tag out of context"
401 pass
402
403 obj.SetProjection(Projection(self.projection_params,
404 self.projection_name,
405 epsg = self.projection_epsg))
406
407 def start_parameter(self, name, qname, attrs):
408 s = attrs.get((None, 'value'))
409 s = str(s) # we can't handle unicode in proj
410 self.projection_params.append(s)
411
412 def start_layer(self, name, qname, attrs, layer_class = Layer):
413 """Start a layer
414
415 Instantiate a layer of class layer_class from the attributes in
416 attrs which may be a dictionary as well as the normal SAX attrs
417 object and bind it to self.aLayer.
418 """
419 title = self.encode(attrs.get((None, 'title'), ""))
420 filename = attrs.get((None, 'filename'), "")
421 filename = os.path.join(self.GetDirectory(), filename)
422 filename = self.encode(filename)
423 visible = self.encode(attrs.get((None, 'visible'), "true")) != "false"
424 fill = parse_color(attrs.get((None, 'fill'), "None"))
425 stroke = parse_color(attrs.get((None, 'stroke'), "#000000"))
426 stroke_width = int(attrs.get((None, 'stroke_width'), "1"))
427 if attrs.has_key((None, "shapestore")):
428 store = self.idmap[attrs[(None, "shapestore")]]
429 else:
430 store = self.theSession.OpenShapefile(filename)
431 self.aLayer = layer_class(title, store,
432 fill = fill, stroke = stroke,
433 lineWidth = stroke_width,
434 visible = visible)
435
436 def end_layer(self, name, qname):
437 self.aMap.AddLayer(self.aLayer)
438 self.aLayer = None
439
440 def start_rasterlayer(self, name, qname, attrs, layer_class = RasterLayer):
441 title = self.encode(attrs.get((None, 'title'), ""))
442 filename = attrs.get((None, 'filename'), "")
443 filename = os.path.join(self.GetDirectory(), filename)
444 filename = self.encode(filename)
445 visible = self.encode(attrs.get((None, 'visible'), "true")) != "false"
446
447 self.aLayer = layer_class(title, filename, visible = visible)
448
449 def end_rasterlayer(self, name, qname):
450 self.aMap.AddLayer(self.aLayer)
451 self.aLayer = None
452
453 def start_classification(self, name, qname, attrs):
454 attrs = self.check_attrs(name, attrs,
455 [AttrDesc("field", True),
456 AttrDesc("field_type", True)])
457 field = attrs["field"]
458 fieldType = attrs["field_type"]
459
460 dbFieldType = self.aLayer.GetFieldType(field)
461
462 if fieldType != dbFieldType:
463 raise ValueError(_("xml field type differs from database!"))
464
465 # setup conversion routines depending on the kind of data
466 # we will be seeing later on
467 if fieldType == FIELDTYPE_STRING:
468 self.conv = str
469 elif fieldType == FIELDTYPE_INT:
470 self.conv = lambda p: int(float(p))
471 elif fieldType == FIELDTYPE_DOUBLE:
472 self.conv = float
473
474 self.aLayer.SetClassificationColumn(field)
475
476 def end_classification(self, name, qname):
477 pass
478
479 def start_clnull(self, name, qname, attrs):
480 self.cl_group = ClassGroupDefault()
481 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
482 self.cl_prop = ClassGroupProperties()
483
484 def end_clnull(self, name, qname):
485 self.cl_group.SetProperties(self.cl_prop)
486 self.aLayer.GetClassification().SetDefaultGroup(self.cl_group)
487 del self.cl_group, self.cl_prop
488
489 def start_clpoint(self, name, qname, attrs):
490 attrib_value = attrs.get((None, 'value'), "0")
491
492 field = self.aLayer.GetClassificationColumn()
493 if self.aLayer.GetFieldType(field) == FIELDTYPE_STRING:
494 value = self.encode(attrib_value)
495 else:
496 value = self.conv(attrib_value)
497 self.cl_group = ClassGroupSingleton(value)
498 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
499 self.cl_prop = ClassGroupProperties()
500
501
502 def end_clpoint(self, name, qname):
503 self.cl_group.SetProperties(self.cl_prop)
504 self.aLayer.GetClassification().AppendGroup(self.cl_group)
505 del self.cl_group, self.cl_prop
506
507 def start_clrange(self, name, qname, attrs):
508
509 range = attrs.get((None, 'range'), None)
510 # for backward compatibility (min/max are not saved)
511 min = attrs.get((None, 'min'), None)
512 max = attrs.get((None, 'max'), None)
513
514 try:
515 if range is not None:
516 self.cl_group = ClassGroupRange(Range(range))
517 elif min is not None and max is not None:
518 self.cl_group = ClassGroupRange((self.conv(min),
519 self.conv(max)))
520 else:
521 self.cl_group = ClassGroupRange(Range(None))
522
523 except ValueError:
524 raise ValueError(_("Classification range is not a number!"))
525
526 self.cl_group.SetLabel(attrs.get((None, 'label'), ""))
527 self.cl_prop = ClassGroupProperties()
528
529
530 def end_clrange(self, name, qname):
531 self.cl_group.SetProperties(self.cl_prop)
532 self.aLayer.GetClassification().AppendGroup(self.cl_group)
533 del self.cl_group, self.cl_prop
534
535 def start_cldata(self, name, qname, attrs):
536 self.cl_prop.SetLineColor(
537 parse_color(attrs.get((None, 'stroke'), "None")))
538 self.cl_prop.SetLineWidth(
539 int(attrs.get((None, 'stroke_width'), "0")))
540 self.cl_prop.SetFill(parse_color(attrs.get((None, 'fill'), "None")))
541
542 def end_cldata(self, name, qname):
543 pass
544
545 def start_labellayer(self, name, qname, attrs):
546 self.aLayer = self.aMap.LabelLayer()
547
548 def start_label(self, name, qname, attrs):
549 x = float(attrs[(None, 'x')])
550 y = float(attrs[(None, 'y')])
551 text = self.encode(attrs[(None, 'text')])
552 halign = attrs[(None, 'halign')]
553 valign = attrs[(None, 'valign')]
554 self.aLayer.AddLabel(x, y, text, halign = halign, valign = valign)
555
556 def characters(self, chars):
557 pass
558
559
560 def load_session(filename, db_connection_callback = None):
561 """Load a Thuban session from the file object file
562
563 The db_connection_callback, if given should be a callable object
564 that can be called like this:
565 db_connection_callback(params, message)
566
567 where params is a dictionary containing the known connection
568 parameters and message is a string with a message why the connection
569 failed. db_connection_callback should return a new dictionary with
570 corrected and perhaps additional parameters like a password or None
571 to indicate that the user cancelled.
572 """
573 handler = SessionLoader(db_connection_callback)
574 handler.read(filename)
575
576 session = handler.theSession
577 # Newly loaded session aren't modified
578 session.UnsetModified()
579
580 handler.Destroy()
581
582 return session
583

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26