/[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 1664 - (show annotations)
Wed Aug 27 15:20:54 2003 UTC (21 years, 6 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/load.py
File MIME type: text/x-python
File size: 22045 byte(s)
As preparation for the 0.9 release, switch thuban files to a
non-dev namespace

* Thuban/Model/save.py (SessionSaver.write_session): Write files
with the http://thuban.intevation.org/dtds/thuban-0.9.dtd
namespace

* Thuban/Model/load.py (SessionLoader.__init__): Accept the
http://thuban.intevation.org/dtds/thuban-0.9.dtd namespace too

* 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 new namespace

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26