/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/Model/session.py
ViewVC logotype

Contents of /branches/WIP-pyshapelib-bramz/Thuban/Model/session.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2734 - (show annotations)
Thu Mar 1 12:42:59 2007 UTC (18 years ago) by bramz
File MIME type: text/x-python
File size: 15639 byte(s)
made a copy
1 # Copyright (c) 2001, 2002, 2003, 2004 by Intevation GmbH
2 # Authors:
3 # Bernhard Herzog <[email protected]>
4 # Jan-Oliver Wagner <[email protected]>
5 #
6 # This program is free software under the GPL (>=v2)
7 # Read the file COPYING coming with Thuban for details.
8
9 __version__ = "$Revision$"
10
11 import os
12 from tempfile import mktemp
13 import weakref
14
15 from messages import MAPS_CHANGED, EXTENSIONS_CHANGED, FILENAME_CHANGED, \
16 MAP_LAYERS_CHANGED, MAP_PROJECTION_CHANGED, \
17 LAYER_CHANGED, LAYER_PROJECTION_CHANGED, LAYER_VISIBILITY_CHANGED,\
18 EXTENSION_CHANGED, EXTENSION_OBJECTS_CHANGED, CHANGED, \
19 TABLE_REMOVED, DBCONN_ADDED, DBCONN_REMOVED
20
21 from Thuban import _
22
23 from base import TitledObject, Modifiable
24 from map import Map
25 from data import ShapefileStore
26 from table import DBFTable
27 import postgisdb
28
29 from transientdb import TransientDatabase, AutoTransientTable
30
31 class AutoRemoveFile:
32
33 """Remove a file once all references go away."""
34
35 def __init__(self, filename, tempdir = None):
36 """Initialize the AutoRemoveFile
37
38 Parameters:
39 filename -- The name of the file to remove in __del__
40 tempdir -- Another object simple stored as an instance variable.
41
42 As the name suggests the tempdir parameter is intended for a
43 temporary directory the file might be located in. The intended
44 use is that it's an instance of AutoRemoveDir.
45 """
46 self.filename = filename
47 self.tempdir = tempdir
48
49 def __del__(self, remove = os.remove):
50 remove(self.filename)
51
52 class AutoRemoveDir:
53
54 """Remove a directory once all references go away
55
56 The intended use of this class together with AutoRemoveFile is for
57 temporary directories and files containd therein. An AutoRemoveDir
58 should be instantiated for the directory and passed as the tempdir
59 parameter to every AutoRemoveFile instance created for files in the
60 directory. An AutoRemoveFile shold be instantiated for every file
61 created in the directory so that the directory is automatically
62 removed once the last file is removed.
63 """
64
65 def __init__(self, filename):
66 """Initialize the AutoRemoveDir
67
68 The parameter is the name of the directory.
69 """
70 self.filename = filename
71
72 def __del__(self, rmdir = os.rmdir):
73 rmdir(self.filename)
74
75
76 # WeakKey dictionary mapping objects like the transient_db to
77 # AutoRemoveDir or AutoRemoveFile instances to make sure that the
78 # temporary files and the directory are deleted but not before the
79 # objects that use them go away.
80 auto_remover = weakref.WeakKeyDictionary()
81
82 class Session(TitledObject, Modifiable):
83
84 """A complete session.
85
86 A Session consists of arbitrary numbers of maps, tables and extensions
87
88 Session objects send the following events:
89
90 TITLE_CHANGED -- The title has changed. Parameters: the session.
91
92 FILENAME_CHANGED -- The filename has changed. No parameters.
93
94 MAPS_CHANGED -- Maps were added, removed.
95
96 EXTENSIONS_CHANGED -- Extensions were added, removed.
97
98 MAP_LAYERS_CHANGED -- Same as the map's event of the same name.
99 It's simply resent from the session to make
100 subscriptions easier.
101
102 CHANGED -- Generic changed event. Parameters: the session. The
103 event is always issued when any other changed event
104 is issused. This is useful for code that needs to be
105 notified whenever something in the session has
106 changed but it's too cumbersome or error-prone to
107 subscribe to all the individual events.
108 """
109
110 # message channels that have to be forwarded from maps contained in
111 # the session.
112 forwarded_channels = (
113 # generic channels
114 CHANGED,
115
116 # map specific channels
117 MAP_PROJECTION_CHANGED,
118 MAP_LAYERS_CHANGED,
119
120 # layer channels forwarded by the map
121 LAYER_PROJECTION_CHANGED,
122 LAYER_CHANGED,
123 LAYER_VISIBILITY_CHANGED,
124
125 # channels forwarded by an extension
126 EXTENSION_CHANGED,
127 EXTENSION_OBJECTS_CHANGED)
128
129 def __init__(self, title):
130 TitledObject.__init__(self, title)
131 Modifiable.__init__(self)
132 self.filename = None
133 self.maps = []
134 self.tables = []
135 self.shapestores = []
136 self.extensions = []
137 self.db_connections = []
138 self.temp_dir = None
139 self.transient_db = None
140
141 def changed(self, channel = None, *args):
142 """Like the inherited version but issue a CHANGED message as well.
143
144 The CHANGED message is only issued if channel given is a
145 different channel than CHANGED.
146 """
147 Modifiable.changed(self, channel, *args)
148 if channel != CHANGED:
149 self.issue(CHANGED, self)
150
151 def SetFilename(self, filename):
152 self.filename = filename
153 self.changed(FILENAME_CHANGED)
154
155 def Maps(self):
156 return self.maps
157
158 def HasMaps(self):
159 return len(self.maps) > 0
160
161 def AddMap(self, map):
162 self.maps.append(map)
163 for channel in self.forwarded_channels:
164 map.Subscribe(channel, self.forward, channel)
165 self.changed(MAPS_CHANGED)
166
167 def RemoveMap(self, map):
168 for channel in self.forwarded_channels:
169 map.Unsubscribe(channel, self.forward, channel)
170 self.maps.remove(map)
171 self.changed(MAPS_CHANGED)
172 map.Destroy()
173
174 def Extensions(self):
175 return self.extensions
176
177 def HasExtensions(self):
178 return len(self.extensions) > 0
179
180 def AddExtension(self, extension):
181 self.extensions.append(extension)
182 for channel in self.forwarded_channels:
183 extension.Subscribe(channel, self.forward, channel)
184 self.changed(EXTENSIONS_CHANGED)
185
186 def ShapeStores(self):
187 """Return a list of all ShapeStore objects open in the session"""
188 return [store() for store in self.shapestores]
189
190 def _add_shapestore(self, store):
191 """Internal: Add the shapestore to the list of shapestores"""
192 self.shapestores.append(weakref.ref(store,
193 self._clean_weak_store_refs))
194
195 def _clean_weak_store_refs(self, weakref):
196 """Internal: Remove the weakref from the shapestores list"""
197 self.shapestores = [store for store in self.shapestores
198 if store is not weakref]
199
200 def Tables(self):
201 """Return a list of all table objects open in the session
202
203 The list includes all tables that are indirectly opened through
204 shape stores and the tables that have been opened explicitly.
205 """
206 tables = self.tables[:]
207 ids = {}
208 for t in tables:
209 ids[id(t)] = 1
210 for store in self.ShapeStores():
211 t = store.Table()
212 if id(t) not in ids:
213 ids[id(t)] = 1
214 tables.append(t)
215 return tables
216
217 def UnreferencedTables(self):
218 """Return the tables that are not referenced by other data sources"""
219 known = {}
220 for table in self.tables:
221 known[id(table)] = 0
222 for table in self.tables + self.ShapeStores():
223 for dep in table.Dependencies():
224 known[id(dep)] = 1
225 return [table for table in self.tables if known[id(table)] == 0]
226
227 def AddTable(self, table):
228 """Add the table to the session
229
230 All tables associated with the session that are not implicitly
231 created by the OpenShapefile method (and maybe other Open*
232 methods in the future) have to be passed to this method to make
233 sure the session knows about it. The session keeps a reference
234 to the table. Only tables managed by the session in this way
235 should be used for layers contained in one of the session's
236 maps.
237
238 The table parameter may be any object implementing the table
239 interface. If it's not already one of the transient tables
240 instantiate an AutoTransientTable with it and use that instead
241 of the original table (note that the AutoTransientTable keeps a
242 reference to the original table).
243
244 Return the table object actually used by the session.
245 """
246 if not hasattr(table, "transient_table"):
247 transient_table = AutoTransientTable(self.TransientDB(), table)
248 else:
249 transient_table = table
250 self.tables.append(transient_table)
251 self.changed()
252 return transient_table
253
254 def RemoveTable(self, table):
255 """Remove the table from the session.
256
257 The table object must be a table object previously returned by
258 the AddTable method. If the table is not part of the session
259 raise a ValueError.
260
261 Issue a TABLE_REMOVED message after the table has been removed.
262 The message has the removed table as the single parameter.
263 """
264 tables = [t for t in self.tables if t is not table]
265 if len(tables) == len(self.tables):
266 raise ValueError
267 self.tables = tables
268 self.changed(TABLE_REMOVED, table)
269
270 def DataContainers(self):
271 """Return all data containers, i.e. shapestores and tables"""
272 return self.tables + self.ShapeStores()
273
274 def OpenTableFile(self, filename):
275 """Open the table file filename and return the table object.
276
277 The filename argument must be the name of a DBF file.
278 """
279 return self.AddTable(DBFTable(filename))
280
281 def temp_directory(self):
282 """
283 Return the name of the directory for session specific temporary files
284
285 Create the directory if it doesn't exist yet.
286 """
287 if self.temp_dir is None:
288 temp_dir = mktemp()
289 os.mkdir(temp_dir, 0700)
290 self.temp_dir = temp_dir
291 self.temp_dir_remover = AutoRemoveDir(self.temp_dir)
292 return self.temp_dir
293
294 def OpenShapefile(self, filename):
295 """Return a shapefile store object for the data in the given file"""
296 store = ShapefileStore(self, filename)
297 self._add_shapestore(store)
298 return store
299
300 def AddShapeStore(self, shapestore):
301 """Add the shapestore to the session.
302
303 The session only holds a weak reference to the shapestore, so it
304 will automatically be removed from the session when the last
305 reference goes away.
306 """
307 self._add_shapestore(shapestore)
308 return shapestore
309
310 def TransientDB(self):
311 if self.transient_db is None:
312 filename = os.path.join(self.temp_directory(), "transientdb")
313 self.transient_db = TransientDatabase(filename)
314 #print self.temp_dir_remover
315 auto_remover[self.transient_db] = AutoRemoveFile(filename,
316 self.temp_dir_remover)
317 return self.transient_db
318
319 def AddDBConnection(self, dbconn):
320 """Add the database connection dbconn to the session
321
322 The argument should be an instance of PostGISConnection.
323 """
324 self.db_connections.append(dbconn)
325 self.changed(DBCONN_ADDED)
326
327 def DBConnections(self):
328 """
329 Return a list of all database connections registered with the session
330 """
331 return self.db_connections
332
333 def HasDBConnections(self):
334 """Return whether the session has open database connections"""
335 return bool(self.db_connections)
336
337 def CanRemoveDBConnection(self, dbconn):
338 """Return whether the database connections dbconn can be removed
339
340 If can be removed if none of the shapestores or tables in the
341 session references it.
342 """
343 for store in self.ShapeStores():
344 if (isinstance(store, postgisdb.PostGISShapeStore)
345 and store.db is dbconn):
346 return 0
347 for table in self.Tables():
348 if (isinstance(table, postgisdb.PostGISTable)
349 and table.db is dbconn):
350 return 0
351 return 1
352
353 def RemoveDBConnection(self, dbconn):
354 """Remove the database connection from the session
355
356 The parameter must be a connection that was registered
357 previously by a AddDBConnection() call.
358 """
359 if self.CanRemoveDBConnection(dbconn):
360 remaining = [c for c in self.db_connections if c is not dbconn]
361 if len(remaining) < len(self.db_connections):
362 self.db_connections = remaining
363 self.changed(DBCONN_REMOVED)
364 else:
365 raise ValueError("DBConection %r is not registered"
366 " with session %r" % (dbconn, self))
367 else:
368 raise ValueError("DBConnection %r is still in use" % (dbconn,))
369
370 def OpenDBShapeStore(self, db, tablename, id_column = None,
371 geometry_column = None):
372 """Create and return a shapstore for a table in the database
373
374 The db parameter must be a database connection previously passed
375 to AddDBConnection().
376 """
377 store = postgisdb.PostGISShapeStore(db, tablename,
378 id_column = id_column,
379 geometry_column = geometry_column)
380 self._add_shapestore(store)
381 return store
382
383 def Destroy(self):
384 for map in self.maps:
385 map.Destroy()
386 self.maps = []
387 self.tables = []
388 Modifiable.Destroy(self)
389
390 # Close the transient DB explicitly so that it removes any
391 # journal files from the temporary directory
392 if self.transient_db is not None:
393 self.transient_db.close()
394
395 def forward(self, *args):
396 """Reissue events.
397
398 If the channel the event is forwarded to is a changed-channel
399 that is not the CHANGED channel issue CHANGED as well. An
400 channel is considered to be a changed-channel if it's name ends
401 with 'CHANGED'.
402 """
403 if len(args) > 1:
404 args = (args[-1],) + args[:-1]
405 apply(self.issue, args)
406 channel = args[0]
407 # It's a bit of a kludge to rely on the channel name for this.
408 if channel.endswith("CHANGED") and channel != CHANGED:
409 self.issue(CHANGED, self)
410
411 def WasModified(self):
412 """Return true if the session or one of the maps was modified"""
413 if self.modified:
414 return 1
415 else:
416 for map in self.maps:
417 if map.WasModified():
418 return 1
419 return 0
420
421 def UnsetModified(self):
422 """Unset the modified flag of the session and the maps"""
423 Modifiable.UnsetModified(self)
424 for map in self.maps:
425 map.UnsetModified()
426
427 def TreeInfo(self):
428 items = []
429 if self.filename is None:
430 items.append(_("Filename:"))
431 else:
432 items.append(_("Filename: %s") % self.filename)
433
434 if self.WasModified():
435 items.append(_("Modified"))
436 else:
437 items.append(_("Unmodified"))
438
439 items.extend(self.maps)
440 items.extend(self.extensions)
441
442 return (_("Session: %s") % self.title, items)
443
444
445 def create_empty_session():
446 """Return an empty session useful as a starting point"""
447 import os
448 session = Session(_('unnamed session'))
449 session.SetFilename(None)
450 session.AddMap(Map(_('unnamed map')))
451 session.UnsetModified()
452 return session

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26