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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1664 - (hide annotations)
Wed Aug 27 15:20:54 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: 22045 byte(s)
As preparation for the 0.9 release, switch thuban files to a
non-dev namespace

* Thuban/Model/save.py (SessionSaver.write_session): Write files
with the http://thuban.intevation.org/dtds/thuban-0.9.dtd
namespace

* Thuban/Model/load.py (SessionLoader.__init__): Accept the
http://thuban.intevation.org/dtds/thuban-0.9.dtd 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 for new namespace

* test/test_load.py (LoadSessionTest.dtd, TestSingleLayer)
(TestLayerVisibility.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 for new
namespace

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26