/[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 1930 - (hide annotations)
Tue Nov 11 13:24:41 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: 22983 byte(s)
(SessionLoader.Destroy): New. Clear all
instance variables to cut cyclic references. The GC would have
collected the loader eventually but it can happen that it doesn't
run at all until thuban is closed (2.3 but not 2.2 tries a bit
harder and forces a collection when the interpreter terminates)
(load_session): Call the handler's Destroy method to make sure
that it gets garbage collected early. Otherwise it will be
collected very late if at all and it holds some references to e.g.
shapestores and the session which can lead to leaks (of e.g. the
temporary files)

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26