/[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 1268 - (show annotations)
Fri Jun 20 16:10:12 2003 UTC (21 years, 8 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/load.py
File MIME type: text/x-python
File size: 17012 byte(s)
* Resources/XML/thuban-0.8.dtd: New DTD for the new file format
version.

* Thuban/Model/save.py (sort_data_stores): New. Make topological
sort of the data sources so they can be written with sources that
data sources that depend on other data sources come after the
sources they depend on.
(SessionSaver.__init__): Add idmap instance variable to map from
objects to the ids used in the file.
(SessionSaver.get_id, SessionSaver.define_id)
(SessionSaver.has_id): New. Methods to manage the idmap
(SessionSaver.write): Use thuban-0.8.dtd
(SessionSaver.write_session): Add a namespace on the session and
write out the data sources before the maps.
(SessionSaver.write_data_containers): New. Write the data
containers.
(SessionSaver.write_layer): Layer elements now refer to a
shapestore and don't contain filenames anymore.

* Thuban/Model/load.py (LoadError): Exception class to raise when
errors in the files are discovered
(SessionLoader.__init__): Define dispatchers for elements with a
thuban-0.8 namespace too.
(SessionLoader.check_attrs): New helper method to check and
convert attributes
(AttrDesc): New. Helper class for SessionLoader.check_attrs
(SessionLoader.start_fileshapesource)
(SessionLoader.start_derivedshapesource)
(SessionLoader.start_filetable, SessionLoader.start_jointable):
Handlers for the new elements in the new fileformat
(SessionLoader.start_layer): Handle the shapestore attribute in
addition to filename.
(SessionLoader.start_table, SessionLoader.end_table): Removed.
They were never used in the old formats and aren't needed for the
new.

* Thuban/Model/session.py (Session.DataContainers): New method to
return all "data containers", i.e. shapestores and tables

* test/xmlsupport.py (SaxEventLister.__init__)
(SaxEventLister.startElementNS, sax_eventlist): Add support to
normalize IDs.

* test/test_xmlsupport.py
(TestEventList.test_even_list_id_normalization): New test case for
id normalization

* test/test_load.py (LoadSessionTest.check_format): Use ID
normalization
(LoadSessionTest.thubanids, LoadSessionTest.thubanidrefs): New
class atrributes used for ID normalization
(TestSingleLayer, TestLayerVisibility, TestLabels.test)
(TestLayerProjection.test, TestRasterLayer.test): Adapt to new
file format
(TestJoinedTable): New test for loading sessions with joined
tables.
(TestLoadError): New. Test whether missing required attributes
cause a LoadError.

* test/test_save.py (SaveSessionTest.thubanids)
(SaveSessionTest.thubanidrefs): New class attributes for ID
normalization in .thuban files.
(SaveSessionTest.compare_xml): Use id-normalization.
(SaveSessionTest.testEmptySession)
(SaveSessionTest.testLayerProjection)
(SaveSessionTest.testRasterLayer)
(SaveSessionTest.testClassifiedLayer): Adapt to new file format.
(SaveSessionTest.testLayerProjection): The filename used was the
same as for testSingleLayer. Use a different one.
(SaveSessionTest.test_dbf_table)
(SaveSessionTest.test_joined_table): New test cases for saving the
new data sources structures.
(TestStoreSort, MockDataStore): Classes to test the sorting of the
data stores for writing.

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26