/[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 2375 - (show annotations)
Sun Oct 3 21:05:30 2004 UTC (20 years, 5 months ago) by jan
Original Path: trunk/thuban/Thuban/Model/load.py
File MIME type: text/x-python
File size: 25237 byte(s)
(SessionLoader.start_cldata): Also parse the size attribute.

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26