~ubuntu-branches/ubuntu/lucid/thuban/lucid

« back to all changes in this revision

Viewing changes to Thuban/Model/save.py

  • Committer: Bazaar Package Importer
  • Author(s): Silke Reimer
  • Date: 2004-01-28 12:47:34 UTC
  • Revision ID: james.westby@ubuntu.com-20040128124734-6xotwcqilok6ngut
Tags: upstream-1.0.0
ImportĀ upstreamĀ versionĀ 1.0.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2001, 2002, 2003 by Intevation GmbH
 
2
# Authors:
 
3
# Jan-Oliver Wagner <jan@intevation.de>
 
4
# Bernhard Herzog <bh@intevation.de>
 
5
# Jonathan Coles <jonathan@intevation.de>
 
6
#
 
7
# This program is free software under the GPL (>=v2)
 
8
# Read the file COPYING coming with Thuban for details.
 
9
 
 
10
"""
 
11
Functions to save a session to a file
 
12
"""
 
13
 
 
14
__version__ = "$Revision: 1.38 $"
 
15
 
 
16
import os
 
17
 
 
18
import Thuban.Lib.fileutil
 
19
 
 
20
from Thuban.Model.layer import Layer, RasterLayer
 
21
 
 
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
 
27
 
 
28
from Thuban.Model.xmlwriter import XMLWriter
 
29
from postgisdb import PostGISConnection, PostGISShapeStore
 
30
 
 
31
def relative_filename(dir, filename):
 
32
    """Return a filename relative to dir for the absolute file name absname.
 
33
 
 
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
 
36
    unchanged.
 
37
    """
 
38
    if dir:
 
39
        return Thuban.Lib.fileutil.relative_filename(dir, filename)
 
40
    else:
 
41
        return filename
 
42
 
 
43
 
 
44
def unify_filename(filename):
 
45
    """Return a 'unified' version of filename
 
46
 
 
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
 
51
    """
 
52
    if os.name == "posix":
 
53
        return filename
 
54
    elif os.name == "nt":
 
55
        return "/".join(filename.split("\\"))
 
56
    else:
 
57
        raise RuntimeError("Unsupported platform for unify_filename: %s"
 
58
                           % os.name)
 
59
 
 
60
def sort_data_stores(stores):
 
61
    """Return a topologically sorted version of the sequence of data containers
 
62
 
 
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.
 
65
    """
 
66
    if not stores:
 
67
        return []
 
68
    processed = {}
 
69
    result = []
 
70
    todo = stores[:]
 
71
    while todo:
 
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
 
75
        # cases easier.
 
76
        container = todo.pop(0)
 
77
        if id(container) in processed:
 
78
            continue
 
79
        deps = [dep for dep in container.Dependencies()
 
80
                    if id(dep) not in processed]
 
81
        if deps:
 
82
            todo.append(container)
 
83
            todo.extend(deps)
 
84
        else:
 
85
            result.append(container)
 
86
            processed[id(container)] = 1
 
87
    return result
 
88
 
 
89
 
 
90
class SessionSaver(XMLWriter):
 
91
 
 
92
    """Class to serialize a session into an XML file.
 
93
 
 
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.
 
100
    """
 
101
 
 
102
 
 
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
 
107
        self.idmap = {}
 
108
 
 
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))
 
112
 
 
113
    def define_id(self, obj, value = None):
 
114
        if value is None:
 
115
            value = "D" + str(id(obj))
 
116
        self.idmap[id(obj)] = value
 
117
        return value
 
118
 
 
119
    def has_id(self, obj):
 
120
        return self.idmap.has_key(id(obj))
 
121
 
 
122
    def prepare_filename(self, filename):
 
123
        """Return the string to use when writing filename to the thuban file
 
124
 
 
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.
 
128
        """
 
129
        return unify_filename(relative_filename(self.dir, filename))
 
130
 
 
131
    def write(self, file_or_filename):
 
132
        XMLWriter.write(self, file_or_filename)
 
133
 
 
134
        self.write_header("session", "thuban-1.0.dtd")
 
135
        self.write_session(self.session)
 
136
        self.close()
 
137
 
 
138
    def write_session(self, session, attrs = None, namespaces = ()):
 
139
        """Write the session and its contents
 
140
 
 
141
        By default, write a session element with the title attribute and
 
142
        call write_map for each map contained in the session.
 
143
 
 
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.
 
147
 
 
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
 
152
        thuban session file.
 
153
        """
 
154
        if attrs is None:
 
155
            attrs = {}
 
156
        attrs["title"] = session.title
 
157
        for name, uri in namespaces:
 
158
            attrs["xmlns:" + name] = uri
 
159
        # default name space
 
160
        attrs["xmlns"] = \
 
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():
 
166
            self.write_map(map)
 
167
        self.close_element("session")
 
168
 
 
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),
 
174
                                    "dbtype": "postgis",
 
175
                                    "host": conn.host,
 
176
                                    "port": conn.port,
 
177
                                    "user": conn.user,
 
178
                                    "dbname": conn.dbname})
 
179
            else:
 
180
                raise ValueError("Can't handle db connection %r" % conn)
 
181
 
 
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]))
 
194
                continue
 
195
 
 
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",
 
206
                                   {"id": idvalue,
 
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",
 
212
                                   {"id": idvalue,
 
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",
 
218
                                   {"id": idvalue,
 
219
                                    "title": container.Title(),
 
220
                                    "filename": filename,
 
221
                                    "filetype": "DBF"})
 
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",
 
227
                                   {"id": idvalue,
 
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()})
 
234
            else:
 
235
                raise ValueError("Can't handle container %r" % container)
 
236
 
 
237
 
 
238
    def write_map(self, map):
 
239
        """Write the map and its contents.
 
240
 
 
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.
 
245
        """
 
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')
 
252
 
 
253
    def write_projection(self, projection):
 
254
        """Write the projection.
 
255
        """
 
256
        if projection and len(projection.params) > 0:
 
257
            attrs = {"name": projection.GetName()}
 
258
            epsg = projection.EPSGCode()
 
259
            if epsg is not None:
 
260
                attrs["epsg"] = epsg
 
261
            self.open_element("projection", attrs)
 
262
            for param in projection.params:
 
263
                self.write_element('parameter value="%s"' % 
 
264
                                   self.encode(param))
 
265
            self.close_element("projection")
 
266
 
 
267
    def write_layer(self, layer, attrs = None):
 
268
        """Write the layer.
 
269
 
 
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.
 
273
        """
 
274
 
 
275
        if attrs is None:
 
276
            attrs = {}
 
277
 
 
278
        attrs["title"]   = layer.title
 
279
        attrs["visible"] = ("false", "true")[int(layer.Visible())]
 
280
 
 
281
        if isinstance(layer, Layer):
 
282
            attrs["shapestore"]   = self.get_id(layer.ShapeStore())
 
283
 
 
284
            lc = layer.GetClassification()
 
285
            attrs["stroke"] = lc.GetDefaultLineColor().hex()
 
286
            attrs["stroke_width"] = str(lc.GetDefaultLineWidth())
 
287
            attrs["fill"] = lc.GetDefaultFill().hex()
 
288
 
 
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")
 
298
 
 
299
    def write_classification(self, layer, attrs = None):
 
300
        """Write Classification information."""
 
301
 
 
302
        if attrs is None:
 
303
            attrs = {}
 
304
 
 
305
        lc = layer.GetClassification()
 
306
 
 
307
        field = layer.GetClassificationColumn()
 
308
 
 
309
        #
 
310
        # there isn't a classification of anything so do nothing
 
311
        #
 
312
        if field is None: return
 
313
 
 
314
        attrs["field"] = field
 
315
        attrs["field_type"] = str(layer.GetFieldType(field))
 
316
        self.open_element("classification", attrs)
 
317
 
 
318
        for g in lc:
 
319
            if isinstance(g, ClassGroupDefault):
 
320
                open_el  = 'clnull label="%s"' % self.encode(g.GetLabel())
 
321
                close_el = 'clnull'
 
322
            elif isinstance(g, ClassGroupSingleton):
 
323
                if layer.GetFieldType(field) == FIELDTYPE_STRING:
 
324
                    value = self.encode(g.GetValue())
 
325
                else:
 
326
                    value = str(g.GetValue())
 
327
                open_el  = 'clpoint label="%s" value="%s"' \
 
328
                           % (self.encode(g.GetLabel()), value)
 
329
                close_el = 'clpoint'
 
330
            elif isinstance(g, ClassGroupRange):
 
331
                open_el  = 'clrange label="%s" range="%s"' \
 
332
                          % (self.encode(g.GetLabel()), str(g.GetRange()))
 
333
                close_el = 'clrange'
 
334
            else:
 
335
                assert False, _("Unsupported group type in classification")
 
336
                continue
 
337
 
 
338
            data = g.GetProperties()
 
339
            dict = {'stroke'      : data.GetLineColor().hex(),
 
340
                    'stroke_width': str(data.GetLineWidth()),
 
341
                    'fill'        : data.GetFill().hex()}
 
342
 
 
343
            self.open_element(open_el)
 
344
            self.write_element("cldata", dict)
 
345
            self.close_element(close_el)
 
346
 
 
347
        self.close_element("classification")
 
348
 
 
349
    def write_label_layer(self, layer):
 
350
        """Write the label layer.
 
351
        """
 
352
        labels = layer.Labels()
 
353
        if labels:
 
354
            self.open_element('labellayer')
 
355
            for label in labels:
 
356
                self.write_element(('label x="%g" y="%g" text="%s"'
 
357
                                    ' halign="%s" valign="%s"')
 
358
                                % (label.x, label.y, 
 
359
                                   self.encode(label.text), 
 
360
                                   label.halign,
 
361
                                   label.valign))
 
362
            self.close_element('labellayer')
 
363
 
 
364
 
 
365
 
 
366
def save_session(session, file, saver_class = None):
 
367
    """Save the session session to a file.
 
368
 
 
369
    The file argument may either be a filename or an open file object.
 
370
 
 
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
 
373
    SessionSaver.
 
374
 
 
375
    If writing the session is successful call the session's
 
376
    UnsetModified method
 
377
    """
 
378
    if saver_class is None:
 
379
        saver_class = SessionSaver
 
380
    saver = saver_class(session)
 
381
    saver.write(file)
 
382
 
 
383
    # after a successful save consider the session unmodified.
 
384
    session.UnsetModified()