/[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 1844 - (show annotations)
Tue Oct 21 10:49:44 2003 UTC (21 years, 4 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/load.py
File MIME type: text/x-python
File size: 22605 byte(s)
(SessionLoader.__init__): Also accept the
thuban-1.0-dev.dtd namespace
(SessionLoader.check_attrs): Allow a callable object as conversion
too
(SessionLoader.start_projection, SessionLoader.end_projection)
(SessionLoader.start_parameter): Handle the epsg attribute and
rename a few instance variables to lower case

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26