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

Diff of /branches/WIP-pyshapelib-bramz/Thuban/Model/load.py

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 439 by jonathan, Thu Feb 27 15:54:05 2003 UTC revision 1268 by bh, Fri Jun 20 16:10:12 2003 UTC
# Line 1  Line 1 
1  # Copyright (C) 2001, 2002 by Intevation GmbH  # Copyright (C) 2001, 2002, 2003 by Intevation GmbH
2  # Authors:  # Authors:
3  # Jan-Oliver Wagner <[email protected]>  # Jan-Oliver Wagner <[email protected]>
4  # Bernhard Herzog <[email protected]>  # Bernhard Herzog <[email protected]>
# Line 13  Parser for thuban session files. Line 13  Parser for thuban session files.
13    
14  __version__ = "$Revision$"  __version__ = "$Revision$"
15    
16  import sys, string, os  import string, os
17    
18  import xml.sax  import xml.sax
19  import xml.sax.handler  import xml.sax.handler
20  from xml.sax import make_parser, ErrorHandler  from xml.sax import make_parser, ErrorHandler, SAXNotRecognizedException
21    
22  from Thuban import _  from Thuban import _
23  from Thuban.common import *  
24    from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
25         FIELDTYPE_STRING
26    
27  from Thuban.Model.session import Session  from Thuban.Model.session import Session
28  from Thuban.Model.map import Map  from Thuban.Model.map import Map
29  from Thuban.Model.layer import Layer  from Thuban.Model.layer import Layer, RasterLayer
30  from Thuban.Model.color import Color  from Thuban.Model.color import Color
31  from Thuban.Model.proj import Projection  from Thuban.Model.proj import Projection
32    from Thuban.Model.range import Range
33  from Thuban.Model.classification import Classification, \  from Thuban.Model.classification import Classification, \
34      ClassGroupDefault, ClassGroupSingleton, ClassGroupRange, ClassGroupMap, \      ClassGroupDefault, ClassGroupSingleton, ClassGroupRange, ClassGroupMap, \
35      ClassGroupProperties      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):  def parse_color(color):
47      """Return the color object for the string color.      """Return the color object for the string color.
# Line 40  def parse_color(color): Line 51  def parse_color(color):
51      """      """
52      color = string.strip(color)      color = string.strip(color)
53      if color == "None":      if color == "None":
54          result = Color.None          result = Color.Transparent
55      elif color[0] == '#':      elif color[0] == '#':
56          if len(color) == 7:          if len(color) == 7:
57              r = string.atoi(color[1:3], 16) / 255.0              r = string.atoi(color[1:3], 16) / 255.0
# Line 54  def parse_color(color): Line 65  def parse_color(color):
65          raise ValueError(_("Invalid color specification %s") % color)          raise ValueError(_("Invalid color specification %s") % color)
66      return result      return result
67    
68    class AttrDesc:
69    
70  class ProcessSession(xml.sax.handler.ContentHandler):      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    
     # Dictionary mapping element names (or (URI, element name) pairs for  
     # documents using namespaces) to method names. The methods should  
     # accept the same parameters as the startElement (or startElementNS)  
     # methods. The start_dispatcher is used by the default startElement  
     # and startElementNS methods to call a method for the open tag of an  
     # element.  
     start_dispatcher = {}  
   
     # end_dispatcher works just like start_dispatcher but it's used by  
     # endElement and endElementNS. The method whose names it maps to  
     # should accept the same parameters as endElement and endElementNS.  
     end_dispatcher = {}  
   
   
     def __init__(self, directory):  
         """Inititialize the Sax handler.  
   
         The directory parameter should be the directory containing the  
         session file. It's needed to interpret embedded relative  
         filenames.  
         """  
         self.directory = directory  
         self.chars = ''  
93          self.theSession = None          self.theSession = None
94          self.aMap = None          self.aMap = None
95          self.aLayer = None          self.aLayer = None
96    
97      def startElementNS(self, name, qname, attrs):          # Map ids used in the thuban file to the corresponding objects
98          """Call the method given for name in self.start_dispatcher          # in the session
99          """          self.idmap = {}
100          if name[0] is None:  
101              method_name = self.start_dispatcher.get(name[1])          dispatchers = {
102          else:              'session'       : ("start_session",        "end_session"),
103              # Dispatch with namespace              'fileshapesource': ("start_fileshapesource", None),
104              method_name = self.start_dispatcher.get(name)              'derivedshapesource': ("start_derivedshapesource", None),
105          if method_name is not None:              'filetable': ("start_filetable", None),
106              getattr(self, method_name)(name, qname, attrs)              '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      def endElementNS(self, name, qname):          XMLReader.AddDispatchers(self, dispatchers)
         """Call the method given for name in self.end_dispatcher  
         """  
         if name[0] is None:  
             method_name = self.end_dispatcher.get(name[1])  
         else:  
             # Dispatch with namespace  
             method_name = self.end_dispatcher.get(name)  
         if method_name is not None:  
             getattr(self, method_name)(name, qname)  
128    
129      def start_session(self, name, qname, attrs):      def start_session(self, name, qname, attrs):
130          self.theSession = Session(attrs.get((None, 'title'), None))          self.theSession = Session(self.encode(attrs.get((None, 'title'),
131      start_dispatcher['session'] = "start_session"                                                          None)))
132    
133      def end_session(self, name, qname):      def end_session(self, name, qname):
134          pass          pass
135      end_dispatcher['session'] = "end_session"  
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):      def start_map(self, name, qname, attrs):
258          """Start a map."""          """Start a map."""
259          self.aMap = Map(attrs.get((None, 'title'), None))          self.aMap = Map(attrs.get((None, 'title'), None))
     start_dispatcher['map'] = "start_map"  
260    
261      def end_map(self, name, qname):      def end_map(self, name, qname):
262          self.theSession.AddMap(self.aMap)          self.theSession.AddMap(self.aMap)
263      end_dispatcher['map'] = "end_map"          self.aMap = None
264    
265      def start_projection(self, name, qname, attrs):      def start_projection(self, name, qname, attrs):
266            self.ProjectionName = self.encode(attrs.get((None, 'name'), None))
267          self.ProjectionParams = [ ]          self.ProjectionParams = [ ]
     start_dispatcher['projection'] = "start_projection"  
268    
269      def end_projection(self, name, qname):      def end_projection(self, name, qname):
270          self.aMap.SetProjection(Projection(self.ProjectionParams))          if self.aLayer is not None:
271      end_dispatcher['projection'] = "end_projection"              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):      def start_parameter(self, name, qname, attrs):
282          s = attrs.get((None, 'value'))          s = attrs.get((None, 'value'))
283          s = str(s) # we can't handle unicode in proj          s = str(s) # we can't handle unicode in proj
284          self.ProjectionParams.append(s)          self.ProjectionParams.append(s)
     start_dispatcher['parameter'] = "start_parameter"  
285    
286      def start_layer(self, name, qname, attrs, layer_class = Layer):      def start_layer(self, name, qname, attrs, layer_class = Layer):
287          """Start a layer          """Start a layer
# Line 144  class ProcessSession(xml.sax.handler.Con Line 290  class ProcessSession(xml.sax.handler.Con
290          attrs which may be a dictionary as well as the normal SAX attrs          attrs which may be a dictionary as well as the normal SAX attrs
291          object and bind it to self.aLayer.          object and bind it to self.aLayer.
292          """          """
293          title = attrs.get((None, 'title'), "")          title = self.encode(attrs.get((None, 'title'), ""))
294          filename = attrs.get((None, 'filename'), "")          filename = attrs.get((None, 'filename'), "")
295          filename = os.path.join(self.directory, filename)          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"))          fill = parse_color(attrs.get((None, 'fill'), "None"))
299          stroke = parse_color(attrs.get((None, 'stroke'), "#000000"))          stroke = parse_color(attrs.get((None, 'stroke'), "#000000"))
300          stroke_width = int(attrs.get((None, 'stroke_width'), "1"))          stroke_width = int(attrs.get((None, 'stroke_width'), "1"))
301          self.aLayer = layer_class(title, filename, fill = fill,          if attrs.has_key((None, "shapestore")):
302                                    stroke = stroke, stroke_width = stroke_width)              store = self.idmap[attrs[(None, "shapestore")]]
303      start_dispatcher['layer'] = "start_layer"          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):      def end_layer(self, name, qname):
311          self.aMap.AddLayer(self.aLayer)          self.aMap.AddLayer(self.aLayer)
312      end_dispatcher['layer'] = "end_layer"          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):      def start_classification(self, name, qname, attrs):
328          self.aLayer.GetClassification().SetField(          field = attrs.get((None, 'field'), None)
329              attrs.get((None, 'field'), None))  
330      start_dispatcher['classification'] = "start_classification"          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):      def end_classification(self, name, qname):
348          pass          pass
     end_dispatcher['classification'] = "end_classification"  
349    
350      def start_clnull(self, name, qname, attrs):      def start_clnull(self, name, qname, attrs):
351          self.cl_group = ClassGroupDefault()          self.cl_group = ClassGroupDefault()
352          self.cl_group.SetLabel(attrs.get((None, 'label'), ""))          self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
353          self.cl_prop = ClassGroupProperties()          self.cl_prop = ClassGroupProperties()
     start_dispatcher['clnull'] = "start_clnull"  
354    
355      def end_clnull(self, name, qname):      def end_clnull(self, name, qname):
356          self.cl_group.SetProperties(self.cl_prop)          self.cl_group.SetProperties(self.cl_prop)
357          self.aLayer.GetClassification().SetDefaultGroup(self.cl_group)          self.aLayer.GetClassification().SetDefaultGroup(self.cl_group)
358          del self.cl_group, self.cl_prop          del self.cl_group, self.cl_prop
     end_dispatcher['clnull'] = "end_clnull"  
359    
360      def start_clpoint(self, name, qname, attrs):      def start_clpoint(self, name, qname, attrs):
361          attrib_value = attrs.get((None, 'value'), "0")          attrib_value = attrs.get((None, 'value'), "0")
362    
363          try:          value = self.conv(attrib_value)
             value  = Str2Num(attrib_value)  
         except:  
             value  = attrib_value  
364    
365          self.cl_group = ClassGroupSingleton(value)          self.cl_group = ClassGroupSingleton(value)
366          self.cl_group.SetLabel(attrs.get((None, 'label'), ""))          self.cl_group.SetLabel(self.encode(attrs.get((None, 'label'), "")))
367          self.cl_prop = ClassGroupProperties()          self.cl_prop = ClassGroupProperties()
368    
     start_dispatcher['clpoint'] = "start_clpoint"  
369    
370      def end_clpoint(self, name, qname):      def end_clpoint(self, name, qname):
371          self.cl_group.SetProperties(self.cl_prop)          self.cl_group.SetProperties(self.cl_prop)
372          self.aLayer.GetClassification().AddGroup(self.cl_group)          self.aLayer.GetClassification().AppendGroup(self.cl_group)
373          del self.cl_group, self.cl_prop          del self.cl_group, self.cl_prop
     end_dispatcher['clpoint'] = "end_clpoint"  
374    
375      def start_clrange(self, name, qname, attrs):      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:          try:
383              min = Str2Num(attrs.get((None, 'min'), "0"))              if range is not None:
384              max = Str2Num(attrs.get((None, 'max'), "0"))                  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:          except ValueError:
391              raise ValueError(_("Classification range is not a number!"))              raise ValueError(_("Classification range is not a number!"))
392    
         self.cl_group = ClassGroupRange(min, max)  
393          self.cl_group.SetLabel(attrs.get((None, 'label'), ""))          self.cl_group.SetLabel(attrs.get((None, 'label'), ""))
394          self.cl_prop = ClassGroupProperties()          self.cl_prop = ClassGroupProperties()
395    
     start_dispatcher['clrange'] = "start_clrange"  
396    
397      def end_clrange(self, name, qname):      def end_clrange(self, name, qname):
398          self.cl_group.SetProperties(self.cl_prop)          self.cl_group.SetProperties(self.cl_prop)
399          self.aLayer.GetClassification().AddGroup(self.cl_group)          self.aLayer.GetClassification().AppendGroup(self.cl_group)
400          del self.cl_group, self.cl_prop          del self.cl_group, self.cl_prop
     end_dispatcher['clrange'] = "end_clrange"  
401    
402      def start_cldata(self, name, qname, attrs):      def start_cldata(self, name, qname, attrs):
403          self.cl_prop.SetStroke(parse_color(attrs.get((None, 'stroke'), "None")))          self.cl_prop.SetLineColor(
404          self.cl_prop.SetStrokeWidth(              parse_color(attrs.get((None, 'stroke'), "None")))
405            self.cl_prop.SetLineWidth(
406              int(attrs.get((None, 'stroke_width'), "0")))              int(attrs.get((None, 'stroke_width'), "0")))
407          self.cl_prop.SetFill(parse_color(attrs.get((None, 'fill'), "None")))          self.cl_prop.SetFill(parse_color(attrs.get((None, 'fill'), "None")))
     start_dispatcher['cldata'] = "start_cldata"  
408    
409      def end_cldata(self, name, qname):      def end_cldata(self, name, qname):
410          pass          pass
     end_dispatcher['cldata'] = "end_cldata"  
   
     def start_table(self, name, qname, attrs):  
         print "table title: %s" % attrs.get('title', None)  
     start_dispatcher['table'] = "start_table"  
   
     def end_table(self, name, qname):  
         pass  
     end_dispatcher['table'] = "end_table"  
411    
412      def start_labellayer(self, name, qname, attrs):      def start_labellayer(self, name, qname, attrs):
413          self.aLayer = self.aMap.LabelLayer()          self.aLayer = self.aMap.LabelLayer()
     start_dispatcher['labellayer'] = "start_labellayer"  
414    
415      def start_label(self, name, qname, attrs):      def start_label(self, name, qname, attrs):
416          x = float(attrs[(None, 'x')])          x = float(attrs[(None, 'x')])
417          y = float(attrs[(None, 'y')])          y = float(attrs[(None, 'y')])
418          text = attrs[(None, 'text')]          text = self.encode(attrs[(None, 'text')])
419          halign = attrs[(None, 'halign')]          halign = attrs[(None, 'halign')]
420          valign = attrs[(None, 'valign')]          valign = attrs[(None, 'valign')]
421          self.aLayer.AddLabel(x, y, text, halign = halign, valign = valign)          self.aLayer.AddLabel(x, y, text, halign = halign, valign = valign)
     start_dispatcher['label'] = "start_label"  
422    
423      def characters(self, chars):      def characters(self, chars):
424          pass          pass
# Line 257  class ProcessSession(xml.sax.handler.Con Line 426  class ProcessSession(xml.sax.handler.Con
426    
427  def load_session(filename):  def load_session(filename):
428      """Load a Thuban session from the file object file"""      """Load a Thuban session from the file object file"""
429      dir = os.path.dirname(filename)  
430      file = open(filename)      handler = SessionLoader()
431      handler = ProcessSession(dir)      handler.read(filename)
   
     parser = make_parser()  
     parser.setContentHandler(handler)  
     parser.setErrorHandler(ErrorHandler())  
     parser.setFeature(xml.sax.handler.feature_namespaces, 1)  
     parser.parse(file)  
432    
433      session = handler.theSession      session = handler.theSession
434      # Newly loaded session aren't modified      # Newly loaded session aren't modified

Legend:
Removed from v.439  
changed lines
  Added in v.1268

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26