/[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 1930 - (show 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 # 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 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
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
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 """
218 normalized = {}
219
220 for d in descr:
221 if d.required and not attrs.has_key(d.fullname):
222 raise LoadError("Element %s requires an attribute %r"
223 % (element, d.name))
224 value = attrs.get(d.fullname, d.default)
225
226 if d.conversion in ("idref", "shapesource"):
227 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 elif d.conversion == "ascii":
246 value = value.encode("ascii")
247 elif d.conversion:
248 # Assume it's a callable
249 value = d.conversion(value)
250
251 normalized[d.name] = value
252 return normalized
253
254 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 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 store = DerivedShapeStore(attrs["shapesource"], attrs["table"])
323 self.theSession.AddShapeStore(store)
324 self.idmap[attrs["id"]] = store
325
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 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 table = TransientJoinedTable(self.theSession.TransientDB(),
366 attrs["left"], attrs["leftcolumn"],
367 attrs["right"], attrs["rightcolumn"],
368 outer_join = outer_join)
369 table.SetTitle(attrs["title"])
370 self.idmap[attrs["id"]] = self.theSession.AddTable(table)
371
372 def start_map(self, name, qname, attrs):
373 """Start a map."""
374 self.aMap = Map(self.encode(attrs.get((None, 'title'), None)))
375
376 def end_map(self, name, qname):
377 self.theSession.AddMap(self.aMap)
378 self.aMap = None
379
380 def start_projection(self, name, qname, attrs):
381 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
389 def end_projection(self, name, qname):
390 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 obj.SetProjection(Projection(self.projection_params,
399 self.projection_name,
400 epsg = self.projection_epsg))
401
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 self.projection_params.append(s)
406
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 title = self.encode(attrs.get((None, 'title'), ""))
415 filename = attrs.get((None, 'filename'), "")
416 filename = os.path.join(self.GetDirectory(), filename)
417 filename = self.encode(filename)
418 visible = self.encode(attrs.get((None, 'visible'), "true")) != "false"
419 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 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 fill = fill, stroke = stroke,
428 lineWidth = stroke_width,
429 visible = visible)
430
431 def end_layer(self, name, qname):
432 self.aMap.AddLayer(self.aLayer)
433 self.aLayer = None
434
435 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 def start_classification(self, name, qname, attrs):
449 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 self.aLayer.SetClassificationColumn(field)
467
468 def end_classification(self, name, qname):
469 pass
470
471 def start_clnull(self, name, qname, attrs):
472 self.cl_group = ClassGroupDefault()
473 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
474 self.cl_prop = ClassGroupProperties()
475
476 def end_clnull(self, name, qname):
477 self.cl_group.SetProperties(self.cl_prop)
478 self.aLayer.GetClassification().SetDefaultGroup(self.cl_group)
479 del self.cl_group, self.cl_prop
480
481 def start_clpoint(self, name, qname, attrs):
482 attrib_value = attrs.get((None, 'value'), "0")
483
484 field = self.aLayer.GetClassificationColumn()
485 if self.aLayer.GetFieldType(field) == FIELDTYPE_STRING:
486 value = self.encode(attrib_value)
487 else:
488 value = self.conv(attrib_value)
489 self.cl_group = ClassGroupSingleton(value)
490 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
491 self.cl_prop = ClassGroupProperties()
492
493
494 def end_clpoint(self, name, qname):
495 self.cl_group.SetProperties(self.cl_prop)
496 self.aLayer.GetClassification().AppendGroup(self.cl_group)
497 del self.cl_group, self.cl_prop
498
499 def start_clrange(self, name, qname, attrs):
500
501 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 try:
507 if range is not None:
508 self.cl_group = ClassGroupRange(Range(range))
509 elif min is not None and max is not None:
510 self.cl_group = ClassGroupRange((self.conv(min),
511 self.conv(max)))
512 else:
513 self.cl_group = ClassGroupRange(Range(None))
514
515 except ValueError:
516 raise ValueError(_("Classification range is not a number!"))
517
518 self.cl_group.SetLabel(attrs.get((None, 'label'), ""))
519 self.cl_prop = ClassGroupProperties()
520
521
522 def end_clrange(self, name, qname):
523 self.cl_group.SetProperties(self.cl_prop)
524 self.aLayer.GetClassification().AppendGroup(self.cl_group)
525 del self.cl_group, self.cl_prop
526
527 def start_cldata(self, name, qname, attrs):
528 self.cl_prop.SetLineColor(
529 parse_color(attrs.get((None, 'stroke'), "None")))
530 self.cl_prop.SetLineWidth(
531 int(attrs.get((None, 'stroke_width'), "0")))
532 self.cl_prop.SetFill(parse_color(attrs.get((None, 'fill'), "None")))
533
534 def end_cldata(self, name, qname):
535 pass
536
537 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 text = self.encode(attrs[(None, 'text')])
544 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 def load_session(filename, db_connection_callback = None):
553 """Load a Thuban session from the file object file
554
555 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 handler.read(filename)
567
568 session = handler.theSession
569 # Newly loaded session aren't modified
570 session.UnsetModified()
571
572 handler.Destroy()
573
574 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