/[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 1642 - (show annotations)
Mon Aug 25 10:54:31 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: 18219 byte(s)
* Thuban/Model/load.py (SessionLoader.check_attrs): Raise a
LoadError when a required attribute is missing. The code used to
be commented out for some reason, but probably should have been
active.

* test/test_load.py (TestLoadError.test): Test the message in the
LoadError too to make sure it really is about the missing
attribute

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 class LoadError(Exception):
42 pass
43
44 from Thuban.Model.xmlreader import XMLReader
45 import resource
46
47 def parse_color(color):
48 """Return the color object for the string color.
49
50 Color may be either 'None' or of the form '#RRGGBB' in the usual
51 HTML color notation
52 """
53 color = string.strip(color)
54 if color == "None":
55 result = Transparent
56 elif color[0] == '#':
57 if len(color) == 7:
58 r = string.atoi(color[1:3], 16) / 255.0
59 g = string.atoi(color[3:5], 16) / 255.0
60 b = string.atoi(color[5:7], 16) / 255.0
61 result = Color(r, g, b)
62 else:
63 raise ValueError(_("Invalid hexadecimal color specification %s")
64 % color)
65 else:
66 raise ValueError(_("Invalid color specification %s") % color)
67 return result
68
69 class AttrDesc:
70
71 def __init__(self, name, required = False, default = "",
72 conversion = None):
73 if not isinstance(name, tuple):
74 fullname = (None, name)
75 else:
76 fullname = name
77 name = name[1]
78 self.name = name
79 self.fullname = fullname
80 self.required = required
81 self.default = default
82 self.conversion = conversion
83
84 # set by the SessionLoader's check_attrs method
85 self.value = None
86
87
88 class SessionLoader(XMLReader):
89
90 def __init__(self):
91 """Inititialize the Sax handler."""
92 XMLReader.__init__(self)
93
94 self.theSession = None
95 self.aMap = None
96 self.aLayer = None
97
98 # Map ids used in the thuban file to the corresponding objects
99 # in the session
100 self.idmap = {}
101
102 dispatchers = {
103 'session' : ("start_session", "end_session"),
104 'fileshapesource': ("start_fileshapesource", None),
105 'derivedshapesource': ("start_derivedshapesource", None),
106 'filetable': ("start_filetable", None),
107 'jointable': ("start_jointable", None),
108
109 'map' : ("start_map", "end_map"),
110 'projection' : ("start_projection", "end_projection"),
111 'parameter' : ("start_parameter", None),
112 'layer' : ("start_layer", "end_layer"),
113 'rasterlayer' : ("start_rasterlayer", "end_rasterlayer"),
114 'classification': ("start_classification", "end_classification"),
115 'clnull' : ("start_clnull", "end_clnull"),
116 'clpoint' : ("start_clpoint", "end_clpoint"),
117 'clrange' : ("start_clrange", "end_clrange"),
118 'cldata' : ("start_cldata", "end_cldata"),
119 'table' : ("start_table", "end_table"),
120 'labellayer' : ("start_labellayer", None),
121 'label' : ("start_label", None)}
122
123 # all dispatchers should be used for the 0.8 and 0.9 namespaces too
124 for xmlns in ("http://thuban.intevation.org/dtds/thuban-0.8.dtd",
125 "http://thuban.intevation.org/dtds/thuban-0.9-dev.dtd"):
126 for key, value in dispatchers.items():
127 dispatchers[(xmlns, key)] = value
128
129 XMLReader.AddDispatchers(self, dispatchers)
130
131 def start_session(self, name, qname, attrs):
132 self.theSession = Session(self.encode(attrs.get((None, 'title'),
133 None)))
134
135 def end_session(self, name, qname):
136 pass
137
138 def check_attrs(self, element, attrs, descr):
139 """Check and convert some of the attributes of an element
140
141 Parameters:
142 element -- The element name
143 attrs -- The attrs mapping as passed to the start_* methods
144 descr -- Sequence of attribute descriptions (AttrDesc instances)
145
146 Return a dictionary containig normalized versions of the
147 attributes described in descr. The keys of that dictionary are
148 the name attributes of the attribute descriptions. The attrs
149 dictionary will not be modified.
150
151 If the attribute is required, i.e. the 'required' attribute of
152 the descrtiption is true, but it is not in attrs, raise a
153 LoadError.
154
155 If the attribute has a default value and it is not present in
156 attrs, use that default value as the value in the returned dict.
157
158 If a conversion is specified, convert the value before putting
159 it into the returned dict. The following conversions are
160 available:
161
162 'filename' -- The attribute is a filename.
163
164 If the filename is a relative name, interpret
165 it relative to the directory containing the
166 .thuban file and make it an absolute name
167
168 'shapestore' -- The attribute is the ID of a shapestore
169 defined earlier in the .thuban file. Look it
170 up self.idmap
171
172 'table' -- The attribute is the ID of a table or shapestore
173 defined earlier in the .thuban file. Look it up
174 self.idmap. If it's the ID of a shapestore the
175 value will be the table of the shapestore.
176 """
177 normalized = {}
178
179 for d in descr:
180 if d.required and not attrs.has_key(d.fullname):
181 raise LoadError("Element %s requires an attribute %r"
182 % (element, d.name))
183 value = attrs.get(d.fullname, d.default)
184
185 if d.conversion == "shapesource":
186 if value in self.idmap:
187 value = self.idmap[value]
188 else:
189 raise LoadError("Element %s requires an already defined ID"
190 " in attribute %r"
191 % (element, d.name))
192 elif d.conversion == "table":
193 if value in self.idmap:
194 value = self.idmap[value]
195 if isinstance(value, ShapefileStore):
196 value = value.Table()
197 else:
198 raise LoadError("Element %s requires an already defined ID"
199 " in attribute %r"
200 % (element, d.name))
201 elif d.conversion == "filename":
202 value = os.path.abspath(os.path.join(self.GetDirectory(),
203 value))
204
205 normalized[d.name] = value
206 return normalized
207
208 def start_fileshapesource(self, name, qname, attrs):
209 attrs = self.check_attrs(name, attrs,
210 [AttrDesc("id", True),
211 AttrDesc("filename", True,
212 conversion = "filename"),
213 AttrDesc("filetype", True)])
214 ID = attrs["id"]
215 filename = attrs["filename"]
216 filetype = attrs["filetype"]
217 if filetype != "shapefile":
218 raise LoadError("shapesource filetype %r not supported" % filetype)
219 self.idmap[ID] = self.theSession.OpenShapefile(filename)
220
221 def start_derivedshapesource(self, name, qname, attrs):
222 attrs = self.check_attrs(name, attrs,
223 [AttrDesc("id", True),
224 AttrDesc("shapesource", True,
225 conversion = "shapesource"),
226 AttrDesc("table", True, conversion="table")])
227 store = DerivedShapeStore(attrs["shapesource"], attrs["table"])
228 self.theSession.AddShapeStore(store)
229 self.idmap[attrs["id"]] = store
230
231 def start_filetable(self, name, qname, attrs):
232 attrs = self.check_attrs(name, attrs,
233 [AttrDesc("id", True),
234 AttrDesc("title", True),
235 AttrDesc("filename", True,
236 conversion = "filename"),
237 AttrDesc("filetype")])
238 filetype = attrs["filetype"]
239 if filetype != "DBF":
240 raise LoadError("shapesource filetype %r not supported" % filetype)
241 table = DBFTable(attrs["filename"])
242 table.SetTitle(attrs["title"])
243 self.idmap[attrs["id"]] = self.theSession.AddTable(table)
244
245 def start_jointable(self, name, qname, attrs):
246 attrs = self.check_attrs(name, attrs,
247 [AttrDesc("id", True),
248 AttrDesc("title", True),
249 AttrDesc("left", True, conversion="table"),
250 AttrDesc("leftcolumn", True),
251 AttrDesc("right", True, conversion="table"),
252 AttrDesc("rightcolumn", True),
253
254 # jointype is required for file
255 # version 0.9 but this attribute
256 # wasn't in the 0.8 version because of
257 # an oversight so we assume it's
258 # optional since we want to handle
259 # both file format versions here.
260 AttrDesc("jointype", False,
261 default="INNER")])
262
263 jointype = attrs["jointype"]
264 if jointype == "LEFT OUTER":
265 outer_join = True
266 elif jointype == "INNER":
267 outer_join = False
268 else:
269 raise LoadError("jointype %r not supported" % jointype )
270 table = TransientJoinedTable(self.theSession.TransientDB(),
271 attrs["left"], attrs["leftcolumn"],
272 attrs["right"], attrs["rightcolumn"],
273 outer_join = outer_join)
274 table.SetTitle(attrs["title"])
275 self.idmap[attrs["id"]] = self.theSession.AddTable(table)
276
277 def start_map(self, name, qname, attrs):
278 """Start a map."""
279 self.aMap = Map(self.encode(attrs.get((None, 'title'), None)))
280
281 def end_map(self, name, qname):
282 self.theSession.AddMap(self.aMap)
283 self.aMap = None
284
285 def start_projection(self, name, qname, attrs):
286 self.ProjectionName = self.encode(attrs.get((None, 'name'), None))
287 self.ProjectionParams = [ ]
288
289 def end_projection(self, name, qname):
290 if self.aLayer is not None:
291 obj = self.aLayer
292 elif self.aMap is not None:
293 obj = self.aMap
294 else:
295 assert False, "projection tag out of context"
296 pass
297
298 obj.SetProjection(
299 Projection(self.ProjectionParams, self.ProjectionName))
300
301 def start_parameter(self, name, qname, attrs):
302 s = attrs.get((None, 'value'))
303 s = str(s) # we can't handle unicode in proj
304 self.ProjectionParams.append(s)
305
306 def start_layer(self, name, qname, attrs, layer_class = Layer):
307 """Start a layer
308
309 Instantiate a layer of class layer_class from the attributes in
310 attrs which may be a dictionary as well as the normal SAX attrs
311 object and bind it to self.aLayer.
312 """
313 title = self.encode(attrs.get((None, 'title'), ""))
314 filename = attrs.get((None, 'filename'), "")
315 filename = os.path.join(self.GetDirectory(), filename)
316 filename = self.encode(filename)
317 visible = self.encode(attrs.get((None, 'visible'), "true")) != "false"
318 fill = parse_color(attrs.get((None, 'fill'), "None"))
319 stroke = parse_color(attrs.get((None, 'stroke'), "#000000"))
320 stroke_width = int(attrs.get((None, 'stroke_width'), "1"))
321 if attrs.has_key((None, "shapestore")):
322 store = self.idmap[attrs[(None, "shapestore")]]
323 else:
324 store = self.theSession.OpenShapefile(filename)
325 self.aLayer = layer_class(title, store,
326 fill = fill, stroke = stroke,
327 lineWidth = stroke_width,
328 visible = visible)
329
330 def end_layer(self, name, qname):
331 self.aMap.AddLayer(self.aLayer)
332 self.aLayer = None
333
334 def start_rasterlayer(self, name, qname, attrs, layer_class = RasterLayer):
335 title = self.encode(attrs.get((None, 'title'), ""))
336 filename = attrs.get((None, 'filename'), "")
337 filename = os.path.join(self.GetDirectory(), filename)
338 filename = self.encode(filename)
339 visible = self.encode(attrs.get((None, 'visible'), "true")) != "false"
340
341 self.aLayer = layer_class(title, filename, visible = visible)
342
343 def end_rasterlayer(self, name, qname):
344 self.aMap.AddLayer(self.aLayer)
345 self.aLayer = None
346
347 def start_classification(self, name, qname, attrs):
348 field = attrs.get((None, 'field'), None)
349
350 fieldType = attrs.get((None, 'field_type'), None)
351 dbFieldType = self.aLayer.GetFieldType(field)
352
353 if fieldType != dbFieldType:
354 raise ValueError(_("xml field type differs from database!"))
355
356 # setup conversion routines depending on the kind of data
357 # we will be seeing later on
358 if fieldType == FIELDTYPE_STRING:
359 self.conv = str
360 elif fieldType == FIELDTYPE_INT:
361 self.conv = lambda p: int(float(p))
362 elif fieldType == FIELDTYPE_DOUBLE:
363 self.conv = float
364
365 self.aLayer.SetClassificationColumn(field)
366
367 def end_classification(self, name, qname):
368 pass
369
370 def start_clnull(self, name, qname, attrs):
371 self.cl_group = ClassGroupDefault()
372 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
373 self.cl_prop = ClassGroupProperties()
374
375 def end_clnull(self, name, qname):
376 self.cl_group.SetProperties(self.cl_prop)
377 self.aLayer.GetClassification().SetDefaultGroup(self.cl_group)
378 del self.cl_group, self.cl_prop
379
380 def start_clpoint(self, name, qname, attrs):
381 attrib_value = attrs.get((None, 'value'), "0")
382
383 field = self.aLayer.GetClassificationColumn()
384 if self.aLayer.GetFieldType(field) == FIELDTYPE_STRING:
385 value = self.encode(attrib_value)
386 else:
387 value = self.conv(attrib_value)
388 self.cl_group = ClassGroupSingleton(value)
389 self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
390 self.cl_prop = ClassGroupProperties()
391
392
393 def end_clpoint(self, name, qname):
394 self.cl_group.SetProperties(self.cl_prop)
395 self.aLayer.GetClassification().AppendGroup(self.cl_group)
396 del self.cl_group, self.cl_prop
397
398 def start_clrange(self, name, qname, attrs):
399
400 range = attrs.get((None, 'range'), None)
401 # for backward compatibility (min/max are not saved)
402 min = attrs.get((None, 'min'), None)
403 max = attrs.get((None, 'max'), None)
404
405 try:
406 if range is not None:
407 self.cl_group = ClassGroupRange(Range(range))
408 elif min is not None and max is not None:
409 self.cl_group = ClassGroupRange((self.conv(min),
410 self.conv(max)))
411 else:
412 self.cl_group = ClassGroupRange(Range(None))
413
414 except ValueError:
415 raise ValueError(_("Classification range is not a number!"))
416
417 self.cl_group.SetLabel(attrs.get((None, 'label'), ""))
418 self.cl_prop = ClassGroupProperties()
419
420
421 def end_clrange(self, name, qname):
422 self.cl_group.SetProperties(self.cl_prop)
423 self.aLayer.GetClassification().AppendGroup(self.cl_group)
424 del self.cl_group, self.cl_prop
425
426 def start_cldata(self, name, qname, attrs):
427 self.cl_prop.SetLineColor(
428 parse_color(attrs.get((None, 'stroke'), "None")))
429 self.cl_prop.SetLineWidth(
430 int(attrs.get((None, 'stroke_width'), "0")))
431 self.cl_prop.SetFill(parse_color(attrs.get((None, 'fill'), "None")))
432
433 def end_cldata(self, name, qname):
434 pass
435
436 def start_labellayer(self, name, qname, attrs):
437 self.aLayer = self.aMap.LabelLayer()
438
439 def start_label(self, name, qname, attrs):
440 x = float(attrs[(None, 'x')])
441 y = float(attrs[(None, 'y')])
442 text = self.encode(attrs[(None, 'text')])
443 halign = attrs[(None, 'halign')]
444 valign = attrs[(None, 'valign')]
445 self.aLayer.AddLabel(x, y, text, halign = halign, valign = valign)
446
447 def characters(self, chars):
448 pass
449
450
451 def load_session(filename):
452 """Load a Thuban session from the file object file"""
453
454 handler = SessionLoader()
455 handler.read(filename)
456
457 session = handler.theSession
458 # Newly loaded session aren't modified
459 session.UnsetModified()
460
461 return session
462

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26