/[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 2449 - (show annotations)
Mon Dec 13 13:37:40 2004 UTC (20 years, 2 months ago) by frank
Original Path: trunk/thuban/Thuban/Model/load.py
File MIME type: text/x-python
File size: 27434 byte(s)
Updated docstring for SessionLoader.open_shapefile

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 # Frank Koormann <[email protected]>
7 #
8 # This program is free software under the GPL (>=v2)
9 # Read the file COPYING coming with GRASS for details.
10
11 """
12 Parser for thuban session files.
13 """
14
15 __version__ = "$Revision$"
16
17 import string, os
18
19 import xml.sax
20 import xml.sax.handler
21 from xml.sax import make_parser, ErrorHandler, SAXNotRecognizedException
22
23 from Thuban import _
24
25 from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
26 FIELDTYPE_STRING
27
28 from Thuban.Model.color import Color, Transparent
29
30 from Thuban.Model.session import Session
31 from Thuban.Model.map import Map
32 from Thuban.Model.layer import Layer, RasterLayer
33 from Thuban.Model.proj import Projection
34 from Thuban.Model.range import Range
35 from Thuban.Model.classification import Classification, \
36 ClassGroupDefault, ClassGroupSingleton, ClassGroupRange, ClassGroupMap, \
37 ClassGroupProperties
38 from Thuban.Model.data import DerivedShapeStore, ShapefileStore
39 from Thuban.Model.table import DBFTable
40 from Thuban.Model.transientdb import TransientJoinedTable
41
42 from Thuban.Model.xmlreader import XMLReader
43 import resource
44
45 import postgisdb
46
47 class LoadError(Exception):
48
49 """Exception raised when the thuban file is corrupted
50
51 Not all cases of corrupted thuban files will lead to this exception
52 but those that are found by checks in the loading code itself are.
53 """
54
55
56 class LoadCancelled(Exception):
57
58 """Exception raised to indicate that loading was interrupted by the user"""
59
60
61 def parse_color(color):
62 """Return the color object for the string color.
63
64 Color may be either 'None' or of the form '#RRGGBB' in the usual
65 HTML color notation
66 """
67 color = string.strip(color)
68 if color == "None":
69 result = Transparent
70 elif color[0] == '#':
71 if len(color) == 7:
72 r = string.atoi(color[1:3], 16) / 255.0
73 g = string.atoi(color[3:5], 16) / 255.0
74 b = string.atoi(color[5:7], 16) / 255.0
75 result = Color(r, g, b)
76 else:
77 raise ValueError(_("Invalid hexadecimal color specification %s")
78 % color)
79 else:
80 raise ValueError(_("Invalid color specification %s") % color)
81 return result
82
83 class AttrDesc:
84
85 def __init__(self, name, required = False, default = "",
86 conversion = None):
87 if not isinstance(name, tuple):
88 fullname = (None, name)
89 else:
90 fullname = name
91 name = name[1]
92 self.name = name
93 self.fullname = fullname
94 self.required = required
95 self.default = default
96 self.conversion = conversion
97
98 # set by the SessionLoader's check_attrs method
99 self.value = None
100
101
102 class SessionLoader(XMLReader):
103
104 def __init__(self, db_connection_callback = None,
105 shapefile_callback = None):
106 """Inititialize the Sax handler."""
107 XMLReader.__init__(self)
108
109 self.db_connection_callback = db_connection_callback
110 self.shapefile_callback = shapefile_callback
111 self.theSession = None
112 self.aMap = None
113 self.aLayer = None
114
115 # Map ids used in the thuban file to the corresponding objects
116 # in the session
117 self.idmap = {}
118
119 dispatchers = {
120 'session' : ("start_session", "end_session"),
121
122 'dbconnection': ("start_dbconnection", None),
123
124 'dbshapesource': ("start_dbshapesource", None),
125 'fileshapesource': ("start_fileshapesource", None),
126 'derivedshapesource': ("start_derivedshapesource", None),
127 'filetable': ("start_filetable", None),
128 'jointable': ("start_jointable", None),
129
130 'map' : ("start_map", "end_map"),
131 'projection' : ("start_projection", "end_projection"),
132 'parameter' : ("start_parameter", None),
133 'layer' : ("start_layer", "end_layer"),
134 'rasterlayer' : ("start_rasterlayer", "end_rasterlayer"),
135 'classification': ("start_classification", "end_classification"),
136 'clnull' : ("start_clnull", "end_clnull"),
137 'clpoint' : ("start_clpoint", "end_clpoint"),
138 'clrange' : ("start_clrange", "end_clrange"),
139 'cldata' : ("start_cldata", "end_cldata"),
140 'table' : ("start_table", "end_table"),
141 'labellayer' : ("start_labellayer", None),
142 'label' : ("start_label", None)}
143
144 # all dispatchers should be used for the 0.8 and 0.9 namespaces too
145 for xmlns in ("http://thuban.intevation.org/dtds/thuban-0.8.dtd",
146 "http://thuban.intevation.org/dtds/thuban-0.9-dev.dtd",
147 "http://thuban.intevation.org/dtds/thuban-0.9.dtd",
148 "http://thuban.intevation.org/dtds/thuban-1.0-dev.dtd",
149 "http://thuban.intevation.org/dtds/thuban-1.0rc1.dtd",
150 "http://thuban.intevation.org/dtds/thuban-1.0.0.dtd",
151 "http://thuban.intevation.org/dtds/thuban-1.1-dev.dtd"):
152 for key, value in dispatchers.items():
153 dispatchers[(xmlns, key)] = value
154
155 XMLReader.AddDispatchers(self, dispatchers)
156
157 def Destroy(self):
158 """Clear all instance variables to cut cyclic references.
159
160 The GC would have collected the loader eventually but it can
161 happen that it doesn't run at all until Thuban is closed (2.3
162 but not 2.2 tries a bit harder and forces a collection when the
163 interpreter terminates)
164 """
165 self.__dict__.clear()
166
167 def start_session(self, name, qname, attrs):
168 self.theSession = Session(self.encode(attrs.get((None, 'title'),
169 None)))
170
171 def end_session(self, name, qname):
172 pass
173
174 def check_attrs(self, element, attrs, descr):
175 """Check and convert some of the attributes of an element
176
177 Parameters:
178 element -- The element name
179 attrs -- The attrs mapping as passed to the start_* methods
180 descr -- Sequence of attribute descriptions (AttrDesc instances)
181
182 Return a dictionary containig normalized versions of the
183 attributes described in descr. The keys of that dictionary are
184 the name attributes of the attribute descriptions. The attrs
185 dictionary will not be modified.
186
187 If the attribute is required, i.e. the 'required' attribute of
188 the descrtiption is true, but it is not in attrs, raise a
189 LoadError.
190
191 If the attribute has a default value and it is not present in
192 attrs, use that default value as the value in the returned dict.
193
194 The value is converted before putting it into the returned dict.
195 The following conversions are available:
196
197 'filename' -- The attribute is a filename.
198
199 If the filename is a relative name, interpret
200 it relative to the directory containing the
201 .thuban file and make it an absolute name
202
203 'shapestore' -- The attribute is the ID of a shapestore
204 defined earlier in the .thuban file. Look it
205 up self.idmap
206
207 'table' -- The attribute is the ID of a table or shapestore
208 defined earlier in the .thuban file. Look it up
209 self.idmap. If it's the ID of a shapestore the
210 value will be the table of the shapestore.
211
212 'idref' -- The attribute is the id of an object defined
213 earlier in the .thuban file. Look it up self.idmap
214
215 'ascii' -- The attribute is converted to a bytestring with
216 ascii encoding.
217
218 a callable -- The attribute value is passed to the callable
219 and the return value is used as the converted
220 value
221
222 If no conversion is specified for an attribute it is converted
223 with self.encode.
224 """
225 normalized = {}
226
227 for d in descr:
228 if d.required and not attrs.has_key(d.fullname):
229 raise LoadError("Element %s requires an attribute %r"
230 % (element, d.name))
231 value = attrs.get(d.fullname, d.default)
232
233 if d.conversion in ("idref", "shapesource"):
234 if value in self.idmap:
235 value = self.idmap[value]
236 else:
237 raise LoadError("Element %s requires an already defined ID"
238 " in attribute %r"
239 % (element, d.name))
240 elif d.conversion == "table":
241 if value in self.idmap:
242 value = self.idmap[value]
243 if isinstance(value, ShapefileStore):
244 value = value.Table()
245 else:
246 raise LoadError("Element %s requires an already defined ID"
247 " in attribute %r"
248 % (element, d.name))
249 elif d.conversion == "filename":
250 value = os.path.abspath(os.path.join(self.GetDirectory(),
251 value))
252 elif d.conversion == "ascii":
253 value = value.encode("ascii")
254 elif d.conversion:
255 # Assume it's a callable
256 value = d.conversion(value)
257 else:
258 value = self.encode(value)
259
260 normalized[d.name] = value
261 return normalized
262
263 def open_shapefile(self, filename):
264 """Open shapefile, with alternative path handling.
265
266 If a shapefile cannot be opened and an IOError is raised, check for
267 an alternative. This alternative can be specified interactively by
268 the user or taken from a list of (potential) locations, depending on
269 the callback implementation.
270
271 The alternative is rechecked. If taken from a list the user
272 has to confirm the alternative.
273 """
274
275 # Flag if the alternative path was specified interactively / from list.
276 from_list = 0
277 while 1:
278 try:
279 store = self.theSession.OpenShapefile(filename)
280 if from_list:
281 # A valid path has been guessed from a list
282 # Let the user confirm - or select an alternative.
283 filename, from_list = self.shapefile_callback(
284 filename, "check")
285 if filename is None:
286 # Selection cancelled
287 raise LoadCancelled
288 elif store.FileName() == filename:
289 # Proposed file has been accepted
290 break
291 else:
292 # the filename has been changed, try the new file
293 pass
294 else:
295 break
296 except IOError:
297 if self.shapefile_callback is not None:
298 filename, from_list = self.shapefile_callback(
299 filename,
300 mode = "search",
301 second_try = from_list)
302 if filename is None:
303 raise LoadCancelled
304 print filename
305 else:
306 raise
307 return store
308
309 def start_dbconnection(self, name, qname, attrs):
310 attrs = self.check_attrs(name, attrs,
311 [AttrDesc("id", True),
312 AttrDesc("dbtype", True),
313 AttrDesc("host", False, ""),
314 AttrDesc("port", False, ""),
315 AttrDesc("user", False, ""),
316 AttrDesc("dbname", True)])
317 ID = attrs["id"]
318 dbtype = attrs["dbtype"]
319 if dbtype != "postgis":
320 raise LoadError("dbtype %r not supported" % filetype)
321
322 del attrs["id"]
323 del attrs["dbtype"]
324
325 # Try to open the connection and if it fails ask the user for
326 # the correct parameters repeatedly.
327 # FIXME: it would be better not to insist on getting a
328 # connection here. We should handle this more like the raster
329 # images where the layers etc still are created but are not
330 # drawn in case Thuban can't use the data for various reasons
331 while 1:
332 try:
333 conn = postgisdb.PostGISConnection(**attrs)
334 break
335 except postgisdb.ConnectionError, val:
336 if self.db_connection_callback is not None:
337 attrs = self.db_connection_callback(attrs, str(val))
338 if attrs is None:
339 raise LoadCancelled
340 else:
341 raise
342
343 self.idmap[ID] = conn
344 self.theSession.AddDBConnection(conn)
345
346 def start_dbshapesource(self, name, qname, attrs):
347 attrs = self.check_attrs(name, attrs,
348 [AttrDesc("id", True),
349 AttrDesc("dbconn", True,
350 conversion = "idref"),
351 AttrDesc("tablename", True,
352 conversion = "ascii"),
353 # id_column and geometry_column were
354 # newly introduced with thuban-1.1.dtd
355 # where they're required. Since we
356 # support the older formats too we
357 # have them optional here.
358 AttrDesc("id_column", False, "gid",
359 conversion = "ascii"),
360 AttrDesc("geometry_column", False,
361 conversion = "ascii")])
362 # The default value of geometry_column to use when instantiating
363 # the db shapestore is None which we currently can't easily use
364 # in check_attrs
365 geometry_column = attrs["geometry_column"]
366 if not geometry_column:
367 geometry_column = None
368 dbopen = self.theSession.OpenDBShapeStore
369 self.idmap[attrs["id"]] = dbopen(attrs["dbconn"], attrs["tablename"],
370 id_column = attrs["id_column"],
371 geometry_column=geometry_column)
372
373 def start_fileshapesource(self, name, qname, attrs):
374 attrs = self.check_attrs(name, attrs,
375 [AttrDesc("id", True),
376 AttrDesc("filename", True,
377 conversion = "filename"),
378 AttrDesc("filetype", True)])
379 ID = attrs["id"]
380 filename = attrs["filename"]
381 filetype = attrs["filetype"]
382 if filetype != "shapefile":
383 raise LoadError("shapesource filetype %r not supported" % filetype)
384 self.idmap[ID] = self.open_shapefile(filename)
385
386 def start_derivedshapesource(self, name, qname, attrs):
387 attrs = self.check_attrs(name, attrs,
388 [AttrDesc("id", True),
389 AttrDesc("shapesource", True,
390 conversion = "shapesource"),
391 AttrDesc("table", True, conversion="table")])
392 store = DerivedShapeStore(attrs["shapesource"], attrs["table"])
393 self.theSession.AddShapeStore(store)
394 self.idmap[attrs["id"]] = store
395
396 def start_filetable(self, name, qname, attrs):
397 attrs = self.check_attrs(name, attrs,
398 [AttrDesc("id", True),
399 AttrDesc("title", True),
400 AttrDesc("filename", True,
401 conversion = "filename"),
402 AttrDesc("filetype")])
403 filetype = attrs["filetype"]
404 if filetype != "DBF":
405 raise LoadError("shapesource filetype %r not supported" % filetype)
406 table = DBFTable(attrs["filename"])
407 table.SetTitle(attrs["title"])
408 self.idmap[attrs["id"]] = self.theSession.AddTable(table)
409
410 def start_jointable(self, name, qname, attrs):
411 attrs = self.check_attrs(name, attrs,
412 [AttrDesc("id", True),
413 AttrDesc("title", True),
414 AttrDesc("left", True, conversion="table"),
415 AttrDesc("leftcolumn", True),
416 AttrDesc("right", True, conversion="table"),
417 AttrDesc("rightcolumn", True),
418
419 # jointype is required for file
420 # version 0.9 but this attribute
421 # wasn't in the 0.8 version because of
422 # an oversight so we assume it's
423 # optional since we want to handle
424 # both file format versions here.
425 AttrDesc("jointype", False,
426 default="INNER")])
427
428 jointype = attrs["jointype"]
429 if jointype == "LEFT OUTER":
430 outer_join = True
431 elif jointype == "INNER":
432 outer_join = False
433 else:
434 raise LoadError("jointype %r not supported" % jointype )
435 table = TransientJoinedTable(self.theSession.TransientDB(),
436 attrs["left"], attrs["leftcolumn"],
437 attrs["right"], attrs["rightcolumn"],
438 outer_join = outer_join)
439 table.SetTitle(attrs["title"])
440 self.idmap[attrs["id"]] = self.theSession.AddTable(table)
441
442 def start_map(self, name, qname, attrs):
443 """Start a map."""
444 self.aMap = Map(self.encode(attrs.get((None, 'title'), None)))
445
446 def end_map(self, name, qname):
447 self.theSession.AddMap(self.aMap)
448 self.aMap = None
449
450 def start_projection(self, name, qname, attrs):
451 attrs = self.check_attrs(name, attrs,
452 [AttrDesc("name", conversion=self.encode),
453 AttrDesc("epsg", default=None,
454 conversion=self.encode)])
455 self.projection_name = attrs["name"]
456 self.projection_epsg = attrs["epsg"]
457 self.projection_params = [ ]
458
459 def end_projection(self, name, qname):
460 if self.aLayer is not None:
461 obj = self.aLayer
462 elif self.aMap is not None:
463 obj = self.aMap
464 else:
465 assert False, "projection tag out of context"
466 pass
467
468 obj.SetProjection(Projection(self.projection_params,
469 self.projection_name,
470 epsg = self.projection_epsg))
471
472 def start_parameter(self, name, qname, attrs):
473 s = attrs.get((None, 'value'))
474 s = str(s) # we can't handle unicode in proj
475 self.projection_params.append(s)
476
477 def start_layer(self, name, qname, attrs, layer_class = Layer):
478 """Start a layer
479
480 Instantiate a layer of class layer_class from the attributes in
481 attrs which may be a dictionary as well as the normal SAX attrs
482 object and bind it to self.aLayer.
483 """
484 title = self.encode(attrs.get((None, 'title'), ""))
485 filename = attrs.get((None, 'filename'), "")
486 filename = os.path.join(self.GetDirectory(), filename)
487 filename = self.encode(filename)
488 visible = self.encode(attrs.get((None, 'visible'), "true")) != "false"
489 fill = parse_color(attrs.get((None, 'fill'), "None"))
490 stroke = parse_color(attrs.get((None, 'stroke'), "#000000"))
491 stroke_width = int(attrs.get((None, 'stroke_width'), "1"))
492 if attrs.has_key((None, "shapestore")):
493 store = self.idmap[attrs[(None, "shapestore")]]
494 else:
495 store = self.open_shapefile(filename)
496
497 self.aLayer = layer_class(title, store,
498 fill = fill, stroke = stroke,
499 lineWidth = stroke_width,
500 visible = visible)
501
502 def end_layer(self, name, qname):
503 self.aMap.AddLayer(self.aLayer)
504 self.aLayer = None
505
506 def start_rasterlayer(self, name, qname, attrs, layer_class = RasterLayer):
507 title = self.encode(attrs.get((None, 'title'), ""))
508 filename = attrs.get((None, 'filename'), "")
509 filename = os.path.join(self.GetDirectory(), filename)
510 filename = self.encode(filename)
511 visible = self.encode(attrs.get((None, 'visible'), "true")) != "false"
512
513 self.aLayer = layer_class(title, filename, visible = visible)
514
515 def end_rasterlayer(self, name, qname):
516 self.aMap.AddLayer(self.aLayer)
517 self.aLayer = None
518
519 def start_classification(self, name, qname, attrs):
520 attrs = self.check_attrs(name, attrs,
521 [AttrDesc("field", True),
522 AttrDesc("field_type", True)])
523 field = attrs["field"]
524 fieldType = attrs["field_type"]
525
526 dbFieldType = self.aLayer.GetFieldType(field)
527
528 if fieldType != dbFieldType:
529 raise ValueError(_("xml field type differs from database!"))
530
531 # setup conversion routines depending on the kind of data
532 # we will be seeing later on
533 if fieldType == FIELDTYPE_STRING:
534 self.conv = str
535 elif fieldType == FIELDTYPE_INT:
536 self.conv = lambda p: int(float(p))
537 elif fieldType == FIELDTYPE_DOUBLE:
538 self.conv = float
539
540 self.aLayer.SetClassificationColumn(field)
541
542 def end_classification(self, name, qname):
543 pass
544
545 def start_clnull(self, name, qname, attrs):
546 self.cl_group = ClassGroupDefault()
547 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
548 self.cl_prop = ClassGroupProperties()
549
550 def end_clnull(self, name, qname):
551 self.cl_group.SetProperties(self.cl_prop)
552 self.aLayer.GetClassification().SetDefaultGroup(self.cl_group)
553 del self.cl_group, self.cl_prop
554
555 def start_clpoint(self, name, qname, attrs):
556 attrib_value = attrs.get((None, 'value'), "0")
557
558 field = self.aLayer.GetClassificationColumn()
559 if self.aLayer.GetFieldType(field) == FIELDTYPE_STRING:
560 value = self.encode(attrib_value)
561 else:
562 value = self.conv(attrib_value)
563 self.cl_group = ClassGroupSingleton(value)
564 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
565 self.cl_prop = ClassGroupProperties()
566
567
568 def end_clpoint(self, name, qname):
569 self.cl_group.SetProperties(self.cl_prop)
570 self.aLayer.GetClassification().AppendGroup(self.cl_group)
571 del self.cl_group, self.cl_prop
572
573 def start_clrange(self, name, qname, attrs):
574
575 range = attrs.get((None, 'range'), None)
576 # for backward compatibility (min/max are not saved)
577 min = attrs.get((None, 'min'), None)
578 max = attrs.get((None, 'max'), None)
579
580 try:
581 if range is not None:
582 self.cl_group = ClassGroupRange(Range(range))
583 elif min is not None and max is not None:
584 self.cl_group = ClassGroupRange((self.conv(min),
585 self.conv(max)))
586 else:
587 self.cl_group = ClassGroupRange(Range(None))
588
589 except ValueError:
590 raise ValueError(_("Classification range is not a number!"))
591
592 self.cl_group.SetLabel(attrs.get((None, 'label'), ""))
593 self.cl_prop = ClassGroupProperties()
594
595
596 def end_clrange(self, name, qname):
597 self.cl_group.SetProperties(self.cl_prop)
598 self.aLayer.GetClassification().AppendGroup(self.cl_group)
599 del self.cl_group, self.cl_prop
600
601 def start_cldata(self, name, qname, attrs):
602 self.cl_prop.SetLineColor(
603 parse_color(attrs.get((None, 'stroke'), "None")))
604 self.cl_prop.SetLineWidth(
605 int(attrs.get((None, 'stroke_width'), "0")))
606 self.cl_prop.SetSize(int(attrs.get((None, 'size'), "5")))
607 self.cl_prop.SetFill(parse_color(attrs.get((None, 'fill'), "None")))
608
609 def end_cldata(self, name, qname):
610 pass
611
612 def start_labellayer(self, name, qname, attrs):
613 self.aLayer = self.aMap.LabelLayer()
614
615 def start_label(self, name, qname, attrs):
616 attrs = self.check_attrs(name, attrs,
617 [AttrDesc("x", True, conversion = float),
618 AttrDesc("y", True, conversion = float),
619 AttrDesc("text", True),
620 AttrDesc("halign", True,
621 conversion = "ascii"),
622 AttrDesc("valign", True,
623 conversion = "ascii")])
624 x = attrs['x']
625 y = attrs['y']
626 text = attrs['text']
627 halign = attrs['halign']
628 valign = attrs['valign']
629 if halign not in ("left", "center", "right"):
630 raise LoadError("Unsupported halign value %r" % halign)
631 if valign not in ("top", "center", "bottom"):
632 raise LoadError("Unsupported valign value %r" % valign)
633 self.aLayer.AddLabel(x, y, text, halign = halign, valign = valign)
634
635 def characters(self, chars):
636 pass
637
638
639 def load_session(filename, db_connection_callback = None,
640 shapefile_callback = None):
641 """Load a Thuban session from the file object file
642
643 The db_connection_callback, if given should be a callable object
644 that can be called like this:
645 db_connection_callback(params, message)
646
647 where params is a dictionary containing the known connection
648 parameters and message is a string with a message why the connection
649 failed. db_connection_callback should return a new dictionary with
650 corrected and perhaps additional parameters like a password or None
651 to indicate that the user cancelled.
652 """
653 handler = SessionLoader(db_connection_callback, shapefile_callback)
654 handler.read(filename)
655
656 session = handler.theSession
657 # Newly loaded session aren't modified
658 session.UnsetModified()
659
660 handler.Destroy()
661
662 return session
663

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26