/[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 2036 - (show annotations)
Mon Dec 22 17:49:43 2003 UTC (21 years, 2 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/load.py
File MIME type: text/x-python
File size: 24075 byte(s)
* setup.py (setup call): 1.0.0, yeah!

* Thuban/version.py (longversion): 1.0.0, yeah!

* Thuban/Model/load.py (SessionLoader.__init__): Accept the
1.0.0 namespace too

* Thuban/Model/save.py (SessionSaver.write_session): Save with
1.0.0 namespace

* test/test_load.py (LoadSessionTest.dtd)
(TestSingleLayer.file_contents)
(TestNonAsciiColumnName.file_contents)
(TestLayerVisibility.file_contents)
(TestClassification.file_contents, TestLabels.file_contents)
(TestLayerProjection.file_contents)
(TestRasterLayer.file_contents, TestJoinedTable.file_contents)
(TestLabelLayer.file_contents, TestPostGISLayer.file_contents)
(TestPostGISLayerPassword.file_contents)
(TestLoadError.file_contents, TestLoadError.test): Update for
1.0.0 namespace

* test/test_save.py (SaveSessionTest.dtd)
(SaveSessionTest.testEmptySession)
(SaveSessionTest.testSingleLayer)
(SaveSessionTest.testLayerProjection)
(SaveSessionTest.testRasterLayer)
(SaveSessionTest.testClassifiedLayer)
(SaveSessionTest.test_dbf_table)
(SaveSessionTest.test_joined_table)
(SaveSessionTest.test_save_postgis): Update for 1.0.0 namespace

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26