/[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 1970 - (show 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 # 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 for key, value in dispatchers.items():
148 dispatchers[(xmlns, key)] = value
149
150 XMLReader.AddDispatchers(self, dispatchers)
151
152 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 def start_session(self, name, qname, attrs):
163 self.theSession = Session(self.encode(attrs.get((None, 'title'),
164 None)))
165
166 def end_session(self, name, qname):
167 pass
168
169 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 The value is converted before putting it into the returned dict.
190 The following conversions are available:
191
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
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
213 a callable -- The attribute value is passed to the callable
214 and the return value is used as the converted
215 value
216
217 If no conversion is specified for an attribute it is converted
218 with self.encode.
219 """
220 normalized = {}
221
222 for d in descr:
223 if d.required and not attrs.has_key(d.fullname):
224 raise LoadError("Element %s requires an attribute %r"
225 % (element, d.name))
226 value = attrs.get(d.fullname, d.default)
227
228 if d.conversion in ("idref", "shapesource"):
229 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 elif d.conversion == "ascii":
248 value = value.encode("ascii")
249 elif d.conversion:
250 # Assume it's a callable
251 value = d.conversion(value)
252 else:
253 value = self.encode(value)
254
255 normalized[d.name] = value
256 return normalized
257
258 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 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 store = DerivedShapeStore(attrs["shapesource"], attrs["table"])
327 self.theSession.AddShapeStore(store)
328 self.idmap[attrs["id"]] = store
329
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 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 table = TransientJoinedTable(self.theSession.TransientDB(),
370 attrs["left"], attrs["leftcolumn"],
371 attrs["right"], attrs["rightcolumn"],
372 outer_join = outer_join)
373 table.SetTitle(attrs["title"])
374 self.idmap[attrs["id"]] = self.theSession.AddTable(table)
375
376 def start_map(self, name, qname, attrs):
377 """Start a map."""
378 self.aMap = Map(self.encode(attrs.get((None, 'title'), None)))
379
380 def end_map(self, name, qname):
381 self.theSession.AddMap(self.aMap)
382 self.aMap = None
383
384 def start_projection(self, name, qname, attrs):
385 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
393 def end_projection(self, name, qname):
394 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 obj.SetProjection(Projection(self.projection_params,
403 self.projection_name,
404 epsg = self.projection_epsg))
405
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 self.projection_params.append(s)
410
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 title = self.encode(attrs.get((None, 'title'), ""))
419 filename = attrs.get((None, 'filename'), "")
420 filename = os.path.join(self.GetDirectory(), filename)
421 filename = self.encode(filename)
422 visible = self.encode(attrs.get((None, 'visible'), "true")) != "false"
423 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 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 fill = fill, stroke = stroke,
432 lineWidth = stroke_width,
433 visible = visible)
434
435 def end_layer(self, name, qname):
436 self.aMap.AddLayer(self.aLayer)
437 self.aLayer = None
438
439 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 def start_classification(self, name, qname, attrs):
453 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
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 self.aLayer.SetClassificationColumn(field)
474
475 def end_classification(self, name, qname):
476 pass
477
478 def start_clnull(self, name, qname, attrs):
479 self.cl_group = ClassGroupDefault()
480 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
481 self.cl_prop = ClassGroupProperties()
482
483 def end_clnull(self, name, qname):
484 self.cl_group.SetProperties(self.cl_prop)
485 self.aLayer.GetClassification().SetDefaultGroup(self.cl_group)
486 del self.cl_group, self.cl_prop
487
488 def start_clpoint(self, name, qname, attrs):
489 attrib_value = attrs.get((None, 'value'), "0")
490
491 field = self.aLayer.GetClassificationColumn()
492 if self.aLayer.GetFieldType(field) == FIELDTYPE_STRING:
493 value = self.encode(attrib_value)
494 else:
495 value = self.conv(attrib_value)
496 self.cl_group = ClassGroupSingleton(value)
497 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
498 self.cl_prop = ClassGroupProperties()
499
500
501 def end_clpoint(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_clrange(self, name, qname, attrs):
507
508 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 try:
514 if range is not None:
515 self.cl_group = ClassGroupRange(Range(range))
516 elif min is not None and max is not None:
517 self.cl_group = ClassGroupRange((self.conv(min),
518 self.conv(max)))
519 else:
520 self.cl_group = ClassGroupRange(Range(None))
521
522 except ValueError:
523 raise ValueError(_("Classification range is not a number!"))
524
525 self.cl_group.SetLabel(attrs.get((None, 'label'), ""))
526 self.cl_prop = ClassGroupProperties()
527
528
529 def end_clrange(self, name, qname):
530 self.cl_group.SetProperties(self.cl_prop)
531 self.aLayer.GetClassification().AppendGroup(self.cl_group)
532 del self.cl_group, self.cl_prop
533
534 def start_cldata(self, name, qname, attrs):
535 self.cl_prop.SetLineColor(
536 parse_color(attrs.get((None, 'stroke'), "None")))
537 self.cl_prop.SetLineWidth(
538 int(attrs.get((None, 'stroke_width'), "0")))
539 self.cl_prop.SetFill(parse_color(attrs.get((None, 'fill'), "None")))
540
541 def end_cldata(self, name, qname):
542 pass
543
544 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 text = self.encode(attrs[(None, 'text')])
551 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 def load_session(filename, db_connection_callback = None):
560 """Load a Thuban session from the file object file
561
562 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 handler.read(filename)
574
575 session = handler.theSession
576 # Newly loaded session aren't modified
577 session.UnsetModified()
578
579 handler.Destroy()
580
581 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