File: session.py

package info (click to toggle)
thuban 1.2.2-14
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 9,176 kB
  • sloc: python: 30,410; ansic: 6,181; xml: 4,234; cpp: 1,595; makefile: 145
file content (452 lines) | stat: -rw-r--r-- 15,646 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# Copyright (c) 2001, 2002, 2003, 2004 by Intevation GmbH
# Authors:
# Bernhard Herzog <bh@intevation.de>
# Jan-Oliver Wagner <jan@intevation.de>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with Thuban for details.

__version__ = "$Revision: 2102 $"

import os
from tempfile import mktemp
import weakref

from messages import MAPS_CHANGED, EXTENSIONS_CHANGED, FILENAME_CHANGED, \
     MAP_LAYERS_CHANGED, MAP_PROJECTION_CHANGED, \
     LAYER_CHANGED, LAYER_PROJECTION_CHANGED, LAYER_VISIBILITY_CHANGED,\
     EXTENSION_CHANGED, EXTENSION_OBJECTS_CHANGED, CHANGED, \
     TABLE_REMOVED, DBCONN_ADDED, DBCONN_REMOVED

from Thuban import _

from base import TitledObject, Modifiable
from map import Map
from data import ShapefileStore
from table import DBFTable
import postgisdb

from transientdb import TransientDatabase, AutoTransientTable

class AutoRemoveFile:

    """Remove a file once all references go away."""

    def __init__(self, filename, tempdir = None):
        """Initialize the AutoRemoveFile

        Parameters:
           filename -- The name of the file to remove in __del__
           tempdir -- Another object simple stored as an instance variable.

        As the name suggests the tempdir parameter is intended for a
        temporary directory the file might be located in. The intended
        use is that it's an instance of AutoRemoveDir.
        """
        self.filename = filename
        self.tempdir = tempdir

    def __del__(self, remove = os.remove):
        remove(self.filename)

class AutoRemoveDir:

    """Remove a directory once all references go away

    The intended use of this class together with AutoRemoveFile is for
    temporary directories and files containd therein. An AutoRemoveDir
    should be instantiated for the directory and passed as the tempdir
    parameter to every AutoRemoveFile instance created for files in the
    directory. An AutoRemoveFile shold be instantiated for every file
    created in the directory so that the directory is automatically
    removed once the last file is removed.
    """

    def __init__(self, filename):
        """Initialize the AutoRemoveDir

        The parameter is the name of the directory.
        """
        self.filename = filename

    def __del__(self, rmdir = os.rmdir):
        rmdir(self.filename)


# WeakKey dictionary mapping objects like the transient_db to
# AutoRemoveDir or AutoRemoveFile instances to make sure that the
# temporary files and the directory are deleted but not before the
# objects that use them go away.
auto_remover = weakref.WeakKeyDictionary()

class Session(TitledObject, Modifiable):

    """A complete session.

    A Session consists of arbitrary numbers of maps, tables and extensions

    Session objects send the following events:

        TITLE_CHANGED -- The title has changed. Parameters: the session.

        FILENAME_CHANGED -- The filename has changed. No parameters.

        MAPS_CHANGED -- Maps were added, removed.

        EXTENSIONS_CHANGED -- Extensions were added, removed.

        MAP_LAYERS_CHANGED -- Same as the map's event of the same name.
                          It's simply resent from the session to make
                          subscriptions easier.

        CHANGED -- Generic changed event. Parameters: the session. The
                   event is always issued when any other changed event
                   is issused. This is useful for code that needs to be
                   notified whenever something in the session has
                   changed but it's too cumbersome or error-prone to
                   subscribe to all the individual events.
    """

    # message channels that have to be forwarded from maps contained in
    # the session.
    forwarded_channels = (
        # generic channels
        CHANGED,

        # map specific channels
        MAP_PROJECTION_CHANGED,
        MAP_LAYERS_CHANGED,

        # layer channels forwarded by the map
        LAYER_PROJECTION_CHANGED,
        LAYER_CHANGED,
        LAYER_VISIBILITY_CHANGED,

        # channels forwarded by an extension
        EXTENSION_CHANGED,
        EXTENSION_OBJECTS_CHANGED)

    def __init__(self, title):
        TitledObject.__init__(self, title)
        Modifiable.__init__(self)
        self.filename = None
        self.maps = []
        self.tables = []
        self.shapestores = []
        self.extensions = []
        self.db_connections = []
        self.temp_dir = None
        self.transient_db = None

    def changed(self, channel = None, *args):
        """Like the inherited version but issue a CHANGED message as well.

        The CHANGED message is only issued if channel given is a
        different channel than CHANGED.
        """
        Modifiable.changed(self, channel, *args)
        if channel != CHANGED:
            self.issue(CHANGED, self)

    def SetFilename(self, filename):
        self.filename = filename
        self.changed(FILENAME_CHANGED)

    def Maps(self):
        return self.maps

    def HasMaps(self):
        return len(self.maps) > 0

    def AddMap(self, map):
        self.maps.append(map)
        for channel in self.forwarded_channels:
            map.Subscribe(channel, self.forward, channel)
        self.changed(MAPS_CHANGED)

    def RemoveMap(self, map):
        for channel in self.forwarded_channels:
            map.Unsubscribe(channel, self.forward, channel)
        self.maps.remove(map)
        self.changed(MAPS_CHANGED)
        map.Destroy()

    def Extensions(self):
        return self.extensions

    def HasExtensions(self):
        return len(self.extensions) > 0

    def AddExtension(self, extension):
        self.extensions.append(extension)
        for channel in self.forwarded_channels:
            extension.Subscribe(channel, self.forward, channel)
        self.changed(EXTENSIONS_CHANGED)

    def ShapeStores(self):
        """Return a list of all ShapeStore objects open in the session"""
        return [store() for store in self.shapestores]

    def _add_shapestore(self, store):
        """Internal: Add the shapestore to the list of shapestores"""
        self.shapestores.append(weakref.ref(store,
                                            self._clean_weak_store_refs))

    def _clean_weak_store_refs(self, weakref):
        """Internal: Remove the weakref from the shapestores list"""
        self.shapestores = [store for store in self.shapestores
                                  if store is not weakref]

    def Tables(self):
        """Return a list of all table objects open in the session

        The list includes all tables that are indirectly opened through
        shape stores and the tables that have been opened explicitly.
        """
        tables = self.tables[:]
        ids = {}
        for t in tables:
            ids[id(t)] = 1
        for store in self.ShapeStores():
            t = store.Table()
            if id(t) not in ids:
                ids[id(t)] = 1
                tables.append(t)
        return tables

    def UnreferencedTables(self):
        """Return the tables that are not referenced by other data sources"""
        known = {}
        for table in self.tables:
            known[id(table)] = 0
        for table in self.tables + self.ShapeStores():
            for dep in table.Dependencies():
                known[id(dep)] = 1
        return [table for table in self.tables if known[id(table)] == 0]

    def AddTable(self, table):
        """Add the table to the session

        All tables associated with the session that are not implicitly
        created by the OpenShapefile method (and maybe other Open*
        methods in the future) have to be passed to this method to make
        sure the session knows about it. The session keeps a reference
        to the table. Only tables managed by the session in this way
        should be used for layers contained in one of the session's
        maps.

        The table parameter may be any object implementing the table
        interface. If it's not already one of the transient tables
        instantiate an AutoTransientTable with it and use that instead
        of the original table (note that the AutoTransientTable keeps a
        reference to the original table).

        Return the table object actually used by the session.
        """
        if not hasattr(table, "transient_table"):
            transient_table = AutoTransientTable(self.TransientDB(), table)
        else:
            transient_table = table
        self.tables.append(transient_table)
        self.changed()
        return transient_table

    def RemoveTable(self, table):
        """Remove the table from the session.

        The table object must be a table object previously returned by
        the AddTable method. If the table is not part of the session
        raise a ValueError.

        Issue a TABLE_REMOVED message after the table has been removed.
        The message has the removed table as the single parameter.
        """
        tables = [t for t in self.tables if t is not table]
        if len(tables) == len(self.tables):
            raise ValueError
        self.tables = tables
        self.changed(TABLE_REMOVED, table)

    def DataContainers(self):
        """Return all data containers, i.e. shapestores and tables"""
        return self.tables + self.ShapeStores()

    def OpenTableFile(self, filename):
        """Open the table file filename and return the table object.

        The filename argument must be the name of a DBF file.
        """
        return self.AddTable(DBFTable(filename))

    def temp_directory(self):
        """
        Return the name of the directory for session specific temporary files

        Create the directory if it doesn't exist yet.
        """
        if self.temp_dir is None:
            temp_dir = mktemp()
            os.mkdir(temp_dir, 0700)
            self.temp_dir = temp_dir
            self.temp_dir_remover = AutoRemoveDir(self.temp_dir)
        return self.temp_dir

    def OpenShapefile(self, filename):
        """Return a shapefile store object for the data in the given file"""
        store = ShapefileStore(self, filename)
        self._add_shapestore(store)
        return store

    def AddShapeStore(self, shapestore):
        """Add the shapestore to the session.

        The session only holds a weak reference to the shapestore, so it
        will automatically be removed from the session when the last
        reference goes away.
        """
        self._add_shapestore(shapestore)
        return shapestore

    def TransientDB(self):
        if self.transient_db is None:
            filename = os.path.join(self.temp_directory(), "transientdb")
            self.transient_db = TransientDatabase(filename)
            #print self.temp_dir_remover
            auto_remover[self.transient_db] = AutoRemoveFile(filename,
                                                        self.temp_dir_remover)
        return self.transient_db

    def AddDBConnection(self, dbconn):
        """Add the database connection dbconn to the session

        The argument should be an instance of PostGISConnection.
        """
        self.db_connections.append(dbconn)
        self.changed(DBCONN_ADDED)

    def DBConnections(self):
        """
        Return a list of all database connections registered with the session
        """
        return self.db_connections

    def HasDBConnections(self):
        """Return whether the session has open database connections"""
        return bool(self.db_connections)

    def CanRemoveDBConnection(self, dbconn):
        """Return whether the database connections dbconn can be removed

        If can be removed if none of the shapestores or tables in the
        session references it.
        """
        for store in self.ShapeStores():
            if (isinstance(store, postgisdb.PostGISShapeStore)
                and store.db is dbconn):
                return 0
        for table in self.Tables():
            if (isinstance(table, postgisdb.PostGISTable)
                and table.db is dbconn):
                return 0
        return 1

    def RemoveDBConnection(self, dbconn):
        """Remove the database connection from the session

        The parameter must be a connection that was registered
        previously by a AddDBConnection() call.
        """
        if self.CanRemoveDBConnection(dbconn):
            remaining = [c for c in self.db_connections if c is not dbconn]
            if len(remaining) < len(self.db_connections):
                self.db_connections = remaining
                self.changed(DBCONN_REMOVED)
            else:
                raise ValueError("DBConection %r is not registered"
                                 " with session %r" % (dbconn, self))
        else:
            raise ValueError("DBConnection %r is still in use" % (dbconn,))

    def OpenDBShapeStore(self, db, tablename, id_column = None,
                         geometry_column = None):
        """Create and return a shapstore for a table in the database

        The db parameter must be a database connection previously passed
        to AddDBConnection().
        """
        store = postgisdb.PostGISShapeStore(db, tablename,
                                            id_column = id_column,
                                            geometry_column = geometry_column)
        self._add_shapestore(store)
        return store

    def Destroy(self):
        for map in self.maps:
            map.Destroy()
        self.maps = []
        self.tables = []
        Modifiable.Destroy(self)

        # Close the transient DB explicitly so that it removes any
        # journal files from the temporary directory
        if self.transient_db is not None:
            self.transient_db.close()

    def forward(self, *args):
        """Reissue events.

        If the channel the event is forwarded to is a changed-channel
        that is not the CHANGED channel issue CHANGED as well. An
        channel is considered to be a changed-channel if it's name ends
        with 'CHANGED'.
        """
        if len(args) > 1:
            args = (args[-1],) + args[:-1]
        apply(self.issue, args)
        channel = args[0]
        # It's a bit of a kludge to rely on the channel name for this.
        if channel.endswith("CHANGED") and channel != CHANGED:
            self.issue(CHANGED, self)

    def WasModified(self):
        """Return true if the session or one of the maps was modified"""
        if self.modified:
            return 1
        else:
            for map in self.maps:
                if map.WasModified():
                    return 1
        return 0

    def UnsetModified(self):
        """Unset the modified flag of the session and the maps"""
        Modifiable.UnsetModified(self)
        for map in self.maps:
            map.UnsetModified()

    def TreeInfo(self):
        items = []
        if self.filename is None:
            items.append(_("Filename:"))
        else:
            items.append(_("Filename: %s") % self.filename)

        if self.WasModified():
            items.append(_("Modified"))
        else:
            items.append(_("Unmodified"))

        items.extend(self.maps)
        items.extend(self.extensions)

        return (_("Session: %s") % self.title, items)


def create_empty_session():
    """Return an empty session useful as a starting point"""
    import os
    session = Session(_('unnamed session'))
    session.SetFilename(None)
    session.AddMap(Map(_('unnamed map')))
    session.UnsetModified()
    return session