/[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 1970 - (hide annotations)
Mon Nov 24 18:36:00 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: 23240 byte(s)
* Thuban/Model/load.py (SessionLoader.check_attrs): If no
converter is specified for an attribute assume it's a string
containing only Latin1 characters. Update doc-string accordingly.
This change should fix many places where unicode objects might
accidentally enter Thuban.

* test/test_load.py (TestNonAsciiColumnName): New test to check
what happens with column names in DBF files that contain non-ascii
characters

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26