1
# Copyright (c) 2001, 2002, 2003 by Intevation GmbH
3
# Jan-Oliver Wagner <jan@intevation.de>
4
# Bernhard Herzog <bh@intevation.de>
5
# Jonathan Coles <jonathan@intevation.de>
7
# This program is free software under the GPL (>=v2)
8
# Read the file COPYING coming with Thuban for details.
11
Functions to save a session to a file
14
__version__ = "$Revision: 1.38 $"
18
import Thuban.Lib.fileutil
20
from Thuban.Model.layer import Layer, RasterLayer
22
from Thuban.Model.classification import \
23
ClassGroupDefault, ClassGroupSingleton, ClassGroupRange, ClassGroupMap
24
from Thuban.Model.transientdb import AutoTransientTable, TransientJoinedTable
25
from Thuban.Model.table import DBFTable, FIELDTYPE_STRING
26
from Thuban.Model.data import DerivedShapeStore, ShapefileStore
28
from Thuban.Model.xmlwriter import XMLWriter
29
from postgisdb import PostGISConnection, PostGISShapeStore
31
def relative_filename(dir, filename):
32
"""Return a filename relative to dir for the absolute file name absname.
34
This is almost the same as the function in fileutil, except that dir
35
can be an empty string in which case filename will be returned
39
return Thuban.Lib.fileutil.relative_filename(dir, filename)
44
def unify_filename(filename):
45
"""Return a 'unified' version of filename
47
The .thuban files should be as platform independent as possible.
48
Since they must contain filenames the filenames have to unified. We
49
unify on unix-like filenames for now, which means we do nothing on a
50
posix system and simply replace backslashes with slashes on windows
52
if os.name == "posix":
55
return "/".join(filename.split("\\"))
57
raise RuntimeError("Unsupported platform for unify_filename: %s"
60
def sort_data_stores(stores):
61
"""Return a topologically sorted version of the sequence of data containers
63
The list is sorted so that data containers that depend on other data
64
containers have higher indexes than the containers they depend on.
72
# It doesn't really matter which if the items of todo is
73
# processed next, but if we take the first one, the order is
74
# preserved to some degree which makes writing some of the test
76
container = todo.pop(0)
77
if id(container) in processed:
79
deps = [dep for dep in container.Dependencies()
80
if id(dep) not in processed]
82
todo.append(container)
85
result.append(container)
86
processed[id(container)] = 1
90
class SessionSaver(XMLWriter):
92
"""Class to serialize a session into an XML file.
94
Applications built on top of Thuban may derive from this class and
95
override or extend the methods to save additional information. This
96
additional information should take the form of additional attributes
97
or elements whose names are prefixed with a namespace. To define a
98
namespace derived classes should extend the write_session method to
99
pass the namespaces to the default implementation.
103
def __init__(self, session):
104
XMLWriter.__init__(self)
105
self.session = session
106
# Map object ids to the ids used in the thuban files
109
def get_id(self, obj):
110
"""Return the id used in the thuban file for the object obj"""
111
return self.idmap.get(id(obj))
113
def define_id(self, obj, value = None):
115
value = "D" + str(id(obj))
116
self.idmap[id(obj)] = value
119
def has_id(self, obj):
120
return self.idmap.has_key(id(obj))
122
def prepare_filename(self, filename):
123
"""Return the string to use when writing filename to the thuban file
125
The returned string is a unified version (only slashes as
126
directory separators, see unify_filename) of filename expressed
127
relative to the directory the .thuban file is written to.
129
return unify_filename(relative_filename(self.dir, filename))
131
def write(self, file_or_filename):
132
XMLWriter.write(self, file_or_filename)
134
self.write_header("session", "thuban-1.0.dtd")
135
self.write_session(self.session)
138
def write_session(self, session, attrs = None, namespaces = ()):
139
"""Write the session and its contents
141
By default, write a session element with the title attribute and
142
call write_map for each map contained in the session.
144
The optional argument attrs is for additional attributes and, if
145
given, should be a mapping from attribute names to attribute
146
values. The values should not be XML-escaped yet.
148
The optional argument namespaces, if given, should be a sequence
149
of (name, URI) pairs. The namespaces are written as namespace
150
attributes into the session element. This is mainly useful for
151
derived classes that need to store additional information in a
156
attrs["title"] = session.title
157
for name, uri in namespaces:
158
attrs["xmlns:" + name] = uri
161
"http://thuban.intevation.org/dtds/thuban-1.0.0.dtd"
162
self.open_element("session", attrs)
163
self.write_db_connections(session)
164
self.write_data_containers(session)
165
for map in session.Maps():
167
self.close_element("session")
169
def write_db_connections(self, session):
170
for conn in session.DBConnections():
171
if isinstance(conn, PostGISConnection):
172
self.write_element("dbconnection",
173
{"id": self.define_id(conn),
178
"dbname": conn.dbname})
180
raise ValueError("Can't handle db connection %r" % conn)
182
def write_data_containers(self, session):
183
containers = sort_data_stores(session.DataContainers())
184
for container in containers:
185
if isinstance(container, AutoTransientTable):
186
# AutoTransientTable instances are invisible in the
187
# thuban files. They're only used internally. To make
188
# sure that containers depending on AutoTransientTable
189
# instances refer to the right real containers we give
190
# the AutoTransientTable instances the same id as the
191
# source they depend on.
192
self.define_id(container,
193
self.get_id(container.Dependencies()[0]))
196
idvalue = self.define_id(container)
197
if isinstance(container, ShapefileStore):
198
self.define_id(container.Table(), idvalue)
199
filename = self.prepare_filename(container.FileName())
200
self.write_element("fileshapesource",
201
{"id": idvalue, "filename": filename,
202
"filetype": "shapefile"})
203
elif isinstance(container, DerivedShapeStore):
204
shapesource, table = container.Dependencies()
205
self.write_element("derivedshapesource",
207
"shapesource": self.get_id(shapesource),
208
"table": self.get_id(table)})
209
elif isinstance(container, PostGISShapeStore):
210
conn = container.DBConnection()
211
self.write_element("dbshapesource",
213
"dbconn": self.get_id(conn),
214
"tablename": container.TableName()})
215
elif isinstance(container, DBFTable):
216
filename = self.prepare_filename(container.FileName())
217
self.write_element("filetable",
219
"title": container.Title(),
220
"filename": filename,
222
elif isinstance(container, TransientJoinedTable):
223
left, right = container.Dependencies()
224
left_field = container.left_field
225
right_field = container.right_field
226
self.write_element("jointable",
228
"title": container.Title(),
229
"right": self.get_id(right),
230
"rightcolumn": right_field,
231
"left": self.get_id(left),
232
"leftcolumn": left_field,
233
"jointype": container.JoinType()})
235
raise ValueError("Can't handle container %r" % container)
238
def write_map(self, map):
239
"""Write the map and its contents.
241
By default, write a map element element with the title
242
attribute, call write_projection to write the projection
243
element, call write_layer for each layer contained in the map
244
and finally call write_label_layer to write the label layer.
246
self.open_element('map title="%s"' % self.encode(map.title))
247
self.write_projection(map.projection)
248
for layer in map.Layers():
249
self.write_layer(layer)
250
self.write_label_layer(map.LabelLayer())
251
self.close_element('map')
253
def write_projection(self, projection):
254
"""Write the projection.
256
if projection and len(projection.params) > 0:
257
attrs = {"name": projection.GetName()}
258
epsg = projection.EPSGCode()
261
self.open_element("projection", attrs)
262
for param in projection.params:
263
self.write_element('parameter value="%s"' %
265
self.close_element("projection")
267
def write_layer(self, layer, attrs = None):
270
The optional argument attrs is for additional attributes and, if
271
given, should be a mapping from attribute names to attribute
272
values. The values should not be XML-escaped yet.
278
attrs["title"] = layer.title
279
attrs["visible"] = ("false", "true")[int(layer.Visible())]
281
if isinstance(layer, Layer):
282
attrs["shapestore"] = self.get_id(layer.ShapeStore())
284
lc = layer.GetClassification()
285
attrs["stroke"] = lc.GetDefaultLineColor().hex()
286
attrs["stroke_width"] = str(lc.GetDefaultLineWidth())
287
attrs["fill"] = lc.GetDefaultFill().hex()
289
self.open_element("layer", attrs)
290
self.write_projection(layer.GetProjection())
291
self.write_classification(layer)
292
self.close_element("layer")
293
elif isinstance(layer, RasterLayer):
294
attrs["filename"] = self.prepare_filename(layer.filename)
295
self.open_element("rasterlayer", attrs)
296
self.write_projection(layer.GetProjection())
297
self.close_element("rasterlayer")
299
def write_classification(self, layer, attrs = None):
300
"""Write Classification information."""
305
lc = layer.GetClassification()
307
field = layer.GetClassificationColumn()
310
# there isn't a classification of anything so do nothing
312
if field is None: return
314
attrs["field"] = field
315
attrs["field_type"] = str(layer.GetFieldType(field))
316
self.open_element("classification", attrs)
319
if isinstance(g, ClassGroupDefault):
320
open_el = 'clnull label="%s"' % self.encode(g.GetLabel())
322
elif isinstance(g, ClassGroupSingleton):
323
if layer.GetFieldType(field) == FIELDTYPE_STRING:
324
value = self.encode(g.GetValue())
326
value = str(g.GetValue())
327
open_el = 'clpoint label="%s" value="%s"' \
328
% (self.encode(g.GetLabel()), value)
330
elif isinstance(g, ClassGroupRange):
331
open_el = 'clrange label="%s" range="%s"' \
332
% (self.encode(g.GetLabel()), str(g.GetRange()))
335
assert False, _("Unsupported group type in classification")
338
data = g.GetProperties()
339
dict = {'stroke' : data.GetLineColor().hex(),
340
'stroke_width': str(data.GetLineWidth()),
341
'fill' : data.GetFill().hex()}
343
self.open_element(open_el)
344
self.write_element("cldata", dict)
345
self.close_element(close_el)
347
self.close_element("classification")
349
def write_label_layer(self, layer):
350
"""Write the label layer.
352
labels = layer.Labels()
354
self.open_element('labellayer')
356
self.write_element(('label x="%g" y="%g" text="%s"'
357
' halign="%s" valign="%s"')
359
self.encode(label.text),
362
self.close_element('labellayer')
366
def save_session(session, file, saver_class = None):
367
"""Save the session session to a file.
369
The file argument may either be a filename or an open file object.
371
The optional argument saver_class is the class to use to serialize
372
the session. By default or if it's None, the saver class will be
375
If writing the session is successful call the session's
378
if saver_class is None:
379
saver_class = SessionSaver
380
saver = saver_class(session)
383
# after a successful save consider the session unmodified.
384
session.UnsetModified()