/[thuban]/trunk/thuban/Extensions/svgexport/svgmapwriter.py
ViewVC logotype

Annotation of /trunk/thuban/Extensions/svgexport/svgmapwriter.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2350 - (hide annotations)
Sat Sep 25 18:29:24 2004 UTC (20 years, 5 months ago) by bernhard
File MIME type: text/x-python
File size: 22614 byte(s)
Added Bernhard R. as Author.
(SetBaseID, SetID, make_id): Improved docstring comments to explain
the interaction of the three functions and the XML id contrains.

1 bh 2082 # Copyright (c) 2001, 2002, 2003, 2004 by Intevation GmbH
2 bh 2074 # Authors:
3     # Markus Rechtien <[email protected]>
4 bernhard 2350 # Bernhard Reiter <[email protected]>
5 bh 2074 #
6     # This program is free software under the GPL (>=v2)
7     # Read the file COPYING coming with Thuban for details.
8    
9    
10     """
11     Classes needed to write a session in SVG format
12     """
13    
14 bh 2082 # For compatibility with python 2.2
15     from __future__ import generators
16 bh 2074
17 bh 2082
18     __version__ = "$Revision$"
19     # $Source$
20     # $Id$
21    
22    
23 bh 2074 # Regular expressions used with Fontnames
24     import re
25     # Combining strings
26     from string import join
27     # We need to determine some object types
28 bh 2082 from types import ListType
29 bernhard 2344
30     from Thuban import _
31 bh 2074 # VirtualDC extends XMLWriter
32     from Thuban.Model.xmlwriter import XMLWriter, escape
33     # Color related classes from the model of thuban
34 bh 2082 from Thuban.Model.color import Transparent, Black
35 bh 2074 # The SVGRenderer is subclass of BaseRenderer
36     from Thuban.UI.baserenderer import BaseRenderer
37    
38     # Basic font map.
39     fontMap = { "Times" : re.compile("Times-Roman.*"),
40     "Helvetica" : re.compile("Helvetica.*"),
41     "Courier" : re.compile("Courier.*"),
42     }
43    
44     # Possible values for svg line joins.
45     svg_joins = {'miter':'miter', 'round':'round', 'bevel':'bevel'}
46     # Possible values for svg line caps.
47     svg_caps = {'':'', 'butt':'butt', 'round':'round', 'square':'square'}
48    
49     #
50     # Some pseudo classes to be compatible with the Baserenderer-class.
51     #
52     class Point:
53 bh 2082 """A simple Point class."""
54 bh 2074 def __init__(self, xp=0, yp=0):
55 bh 2082 """Init the point object."""
56 bh 2074 self.x = xp
57     self.y = yp
58    
59     class Trafo:
60 bh 2082 """Class for tranformation properties transfer."""
61 bh 2074 def __init__(self):
62 bh 2082 """Initialize the class."""
63 bh 2074 self.trafos = []
64    
65     def Append(self, type, coeffs):
66 bh 2082 """Append a transformation to the list."""
67 bh 2074 self.trafos.append((type, coeffs))
68    
69     def Count(self):
70 bh 2082 """Get the number of transformations in list."""
71 bh 2074 return len(self.trafos)
72    
73     def Pop(self):
74 bh 2082 """Pop and return a transformation from the end of the list."""
75 bh 2074 if len(self.trafos) > 0:
76     return self.trafos.pop()
77     else: return None
78    
79     class Pattern:
80 bh 2082 """Pattern object """
81 bh 2074 def __init__(self, solid=1):
82 bh 2082 """Init the Pattern object."""
83 bh 2074 self.solid = solid
84    
85     class Pen:
86 bh 2082 """Pen object for property transfer."""
87 bh 2074 def __init__(self, pcolor = Black, pwidth = 1, pdashes = None):
88     self.color = pcolor
89     self.width = pwidth
90     self.dashes = pdashes
91     self.join = 'round'
92     self.cap = 'round'
93    
94     def GetColor(self):
95 bh 2082 """Return the pen's color."""
96 bh 2074 return self.color
97    
98     def GetWidth(self):
99 bh 2082 """Return the pen's width."""
100 bh 2074 return self.width
101    
102     def GetJoin(self):
103 bh 2082 """Return the pen's join type."""
104 bh 2074 return self.join
105    
106     def GetCap(self):
107 bh 2082 """Return the pen's cap type."""
108 bh 2074 return self.cap
109    
110     def GetDashes(self):
111 bh 2082 """Return the pen's dashes."""
112 bh 2074 if self.dashes is None or self.dashes is SOLID:
113     return []
114     else: return self.dashes
115    
116     class Brush:
117 bh 2082 """Brush property class."""
118 bh 2074 def __init__(self, bfill=Black, bpattern=None):
119 bh 2082 """Init the brush with the given values."""
120 bh 2074 self.fill = bfill
121     self.pattern = bpattern
122    
123     def GetColor(self):
124 bh 2082 """Return the brush color."""
125 bh 2074 return self.fill
126    
127     def GetPattern(self):
128 bh 2082 """Return the Brush pattern object."""
129 bh 2074 return self.pattern
130    
131     class Font:
132 bh 2082 """Font class that accts as property object."""
133 bh 2074 def __init__(self, ffamily='Helvetica', fsize=12):
134 bh 2082 """Init the font with the given values."""
135 bh 2074 self.family = ffamily
136     self.size = fsize
137    
138     def GetFaceName(self):
139 bh 2082 """Return the fontfamily the font belongs to."""
140 bh 2074 return self.family
141    
142     def GetPointSize(self):
143 bh 2082 """Return the size of the font in points."""
144 bh 2074 return self.size
145    
146     # Instantiate an empty pen.
147     TRANSPARENT_PEN = Pen(None, 0, None)
148     # Instantiate an empty brush.
149     TRANSPARENT_BRUSH = Brush(None, None)
150     # Instantiate a solid pattern.
151     SOLID = Pattern()
152    
153 bernhard 2344 class SVGMapWriterError(Exception):
154     """Get raised for problems when writing map-svg files.
155    
156     Occasion when this exception is raised:
157     Two layers have the same name to be used as BaseId: Name Clash
158     """
159    
160    
161 bh 2074 class SVGRenderer(BaseRenderer):
162     """Class to render a map onto a VirtualDC.
163    
164     This class, derived from BaseRenderer, will render a hole
165     session onto the VirtualDC to write all shapes as SVG code
166     to a file.
167     In opposite to other renderers it includes metadata, such as
168     shape ids and classification, when rendering the shapes.
169     """
170     def __init__(self, dc, map, scale, offset, region,
171     resolution = 1.0, honor_visibility = 1):
172 bh 2082 """Init SVGRenderer and call superclass init."""
173 bh 2074 BaseRenderer.__init__(self, dc, map, scale, offset, region,
174     resolution, honor_visibility)
175     #
176     self.factor = (abs(region[2]) + abs(region[3])) / (2.0 * 1000.0)
177 bernhard 2344 self.used_baseids=[] # needed for name clash check
178    
179 bh 2074 def make_point(self, x, y):
180 bh 2082 """Return a Point object from two values."""
181 bh 2074 return Point(x, y)
182    
183     def label_font(self):
184     """Return the font object for the label layer"""
185     return Font()
186    
187     def tools_for_property(self, prop):
188 bh 2082 """Return a pen/brush tuple build from a property object."""
189 bh 2074 fill = prop.GetFill()
190     if fill is Transparent:
191     brush = TRANSPARENT_BRUSH
192     else:
193     brush = Brush(fill, SOLID)
194    
195     stroke = prop.GetLineColor()
196     if stroke is Transparent:
197     pen = TRANSPARENT_PEN
198     else:
199     pen = Pen(stroke, prop.GetLineWidth() * self.factor, SOLID)
200     return pen, brush
201    
202     def draw_polygon_shape(self, layer, points, pen, brush):
203     """Draw a polygon shape from layer with the given brush and pen
204    
205     The shape is given by points argument which is a the return
206     value of the shape's Points() method. The coordinates in the
207     DC's coordinate system are determined with
208     self.projected_points.
209     """
210     points = self.projected_points(layer, points)
211    
212     if brush is not TRANSPARENT_BRUSH:
213     self.dc.SetBrush(brush)
214     self.dc.SetPen(pen)
215     for part in points:
216     self.dc.DrawLines(part)
217    
218     def draw_point_shape(self, layer, points, pen, brush):
219     """Draw a point shape from layer with the given brush and pen
220    
221     The shape is given by points argument which is a the return
222     value of the shape's Points() method. The coordinates in the
223     DC's coordinate system are determined with
224     self.projected_points.
225    
226     The point is drawn as a circle centered on the point.
227     """
228     points = self.projected_points(layer, points)
229     if not points:
230     return
231    
232     radius = self.factor * 2.0
233     self.dc.SetBrush(brush)
234     self.dc.SetPen(pen)
235     for part in points:
236     for p in part:
237     self.dc.DrawCircle(p.x - radius, p.y - radius,
238     2.0 * radius)
239    
240     def draw_shape_layer_incrementally(self, layer):
241     """Draw a shapelayer incrementally.
242     """
243     dc = self.dc
244     brush = TRANSPARENT_BRUSH
245     pen = TRANSPARENT_PEN
246 bernhard 2344
247 bh 2074 value = None
248     field = None
249     lc = layer.GetClassification()
250     field = layer.GetClassificationColumn()
251     defaultGroup = lc.GetDefaultGroup()
252     table = layer.ShapeStore().Table()
253    
254     if lc.GetNumGroups() == 0:
255     # There's only the default group, so we can pretend that
256     # there is no field to classifiy on which makes things
257     # faster since we don't need the attribute information at
258     # all.
259     field = None
260    
261     # Determine which render function to use.
262     useraw, draw_func, draw_func_param = \
263     self.low_level_renderer(layer)
264     tool_cache = {}
265    
266 bernhard 2344 if layer.title in self.used_baseids:
267     raise SVGMapWriterError(_("Clash of layer names!\n")+ \
268     _("Two layers probably have the same name, try renaming one."))
269     # prefix of a shape id to be unique
270 bh 2074 dc.SetBaseID(layer.title)
271 bernhard 2344 self.used_baseids.append(layer.title)
272 bh 2074 # Titel of current layer to the groups meta informations
273     dc.BeginGroup(meta={'Layer':layer.Title(), })
274     # Delete all MetaData
275     dc.FlushMeta()
276     for shape in self.layer_shapes(layer):
277     if field is None:
278     group = defaultGroup
279     value = group.GetDisplayText()
280     else:
281     value = table.ReadValue(shape.ShapeID(), field)
282     group = lc.FindGroup(value)
283    
284     if not group.IsVisible():
285     continue
286    
287     # Render classification
288     shapeType = layer.ShapeType()
289     props = group.GetProperties()
290    
291     # put meta infos into DC
292     if field and value:
293     dc.SetMeta({field:value, })
294     # set current shape id
295     dc.SetID(shape.ShapeID())
296    
297     try:
298     pen, brush = tool_cache[id(group)]
299     except KeyError:
300     pen, brush = tool_cache[id(group)] \
301     = self.tools_for_property(group.GetProperties())
302    
303     if useraw:
304     data = shape.RawData()
305     else:
306     data = shape.Points()
307     draw_func(draw_func_param, data, pen, brush)
308     # compatibility
309     if 0:
310     yield True
311     # reset shape id
312     dc.SetID(-1)
313     dc.SetBaseID("")
314     dc.EndGroup()
315    
316     def draw_raster_layer(self, layer):
317 bh 2082 """Draw the raster layer"""
318 bh 2074 # For now we cannot draw raster layers onto our VirtualDC
319     pass
320    
321     def draw_raster_data(self, data, format="BMP"):
322     """Draw the raster image in data onto the DC"""
323     # For now we cannot draw raster data onto our VirtualDC
324     pass
325    
326     def RenderMap(self, selected_layer, selected_shapes):
327 bh 2082 """Overriden to avoid automatic rendering of legend,
328 bh 2074 scalbar and frame.
329 bh 2082 """
330 bh 2074 dc = self.dc
331     self.selected_layer = selected_layer
332     self.selected_shapes = selected_shapes
333     minx, miny, width, height = self.region
334     # scale down to a size of 1000
335     trans = Trafo()
336     trans.Append('scale', (1000.0 / ((width + height) / 2.0)))
337     #
338     dc.BeginClipPath('mapclip')
339     dc.DrawRectangle(0, 0, width, height)
340     dc.EndClipPath()
341     #
342     dc.BeginGroup(meta={'Object':'map', }, clipid='mapclip', \
343     transform=trans)
344     self.render_map()
345     dc.EndGroup()
346    
347    
348     class VirtualDC(XMLWriter):
349 bh 2082 """This class imitates a DC and writes SVG instead.
350 bh 2074
351     All shapes and graphic objects will be turned into
352     SVG elements and will be written into a file.
353     Any properties, such as stroke width or stroke color,
354     will be written together with the SVG elementents.
355 bh 2082 """
356 bh 2074 def __init__(self, file, dim=(0,0), units=''):
357 bh 2082 """Setup some variables and objects for property collection."""
358 bh 2074 XMLWriter.__init__(self)
359     self.dim = dim
360     self.units = units
361     self.pen = {}
362     self.brush = {}
363     self.font = {}
364     self.meta = {}
365     self.style = {}
366     # Some buffers
367     self.points = []
368     self.id = -1
369     self.flush_meta = 1
370     self.write(file)
371    
372     def write_indent(self, str):
373 bh 2082 """Write a string to the file with the current indention level.
374     """
375 bh 2074 from Thuban.Model.xmlwriter import TAB
376     self.file.write("%s%s" % (TAB*self.indent_level, str))
377    
378     def AddMeta(self, key, val):
379 bh 2082 """Append some metadata to the array that will be
380 bh 2074 written with the next svg-element
381 bh 2082 """
382 bh 2074 if key is '' or val is '':
383     return
384     self.meta[key] = val
385    
386     def SetMeta(self, pairs, flush_after=1):
387 bh 2082 """Delete old meta informations and set the new ones."""
388 bh 2074 self.meta = {}
389     self.flush_meta = flush_after
390     for key, val in pairs.items():
391     self.AddMeta(key, val)
392    
393     def FlushMeta(self):
394 bh 2082 """Drop collected metadata."""
395 bh 2074 self.meta = {}
396    
397     def BeginGroup(self, **args):
398 bh 2082 """Begin a group of elements.
399 bh 2074
400     Possible arguments:
401     meta A list of key, value metadata pairs
402     style A list of key, value style attributes
403     clipid The ID of a clipPath definition to be
404     applied to this group
405 bh 2082 """
406 bh 2074 self.FlushMeta()
407     # adding meta data
408     if args.has_key('meta'):
409     for key, val in args['meta'].items():
410     self.AddMeta(key, val)
411     attribs = " "
412     # adding style attributes
413     if args.has_key('style'):
414     for key, val in args['style'].items():
415     attribs += '%s="%s" ' % (key, val)
416     # adding clip informations
417     if args.has_key("clipid"):
418     attribs += ' clip-path="url(#%s)"' % (args['clipid'],)
419     # FIXME: this shouldn't be static
420     attribs += ' clip-rule="evenodd"'
421     if args.has_key('transform'):
422     trafostr = self.parse_trafo(args['transform'])
423     if trafostr:
424     attribs += ' transform="%s"' % (trafostr)
425     # put everything together
426     self.write_indent('<g %s%s>\n' % (self.make_meta(), attribs))
427     self.indent_level += 1
428    
429     def parse_trafo(self, trafo):
430 bh 2082 """Examine a trafo object for asigned transformations details."""
431 bh 2074 if not trafo:
432     return ''
433     retval = ''
434     while trafo.Count() > 0:
435     trans, coeffs = tuple(trafo.Pop())
436     if isinstance(coeffs, ListType):
437     retval += " %s%s" % (trans, join(coeffs, ', '))
438     else: retval += " %s(%s)" % (trans, coeffs)
439     # return the string
440     return retval
441    
442     def EndGroup(self):
443 bh 2082 """End a group of elements"""
444 bh 2074 self.indent_level -= 1
445     self.write_indent('</g>\n')
446     self.FlushMeta()
447    
448     def BeginExport(self):
449 bh 2082 """Start the export process and write basic document
450 bh 2074 informations to the file.
451 bh 2082 """
452 bh 2074 self.write_indent('<?xml version="1.0" encoding="ISO-8859-1" '
453     'standalone="yes"?>\n')
454     width, height = self.dim
455     self.write_indent('<svg>\n')
456     self.indent_level += 1
457    
458     def EndExport(self):
459 bh 2082 """End the export process with closing the SVG tag and close
460     the file accessor"""
461 bh 2074 self.indent_level -= 1
462     self.write_indent('</svg>\n')
463     self.close()
464    
465     def Close(self):
466 bh 2082 """Close the file."""
467 bh 2074 self.close()
468    
469     def BeginDrawing(self):
470 bh 2082 """Dummy function to work with the Thuban renderers."""
471 bh 2074 pass
472    
473     def EndDrawing(self):
474 bh 2082 """Dummy function to work with the Thuban renderers."""
475 bh 2074 pass
476    
477     def GetSizeTuple(self):
478 bh 2082 """Return the dimension of this virtual canvas."""
479 bh 2074 return self.dim
480    
481     def GetTextExtent(self, text):
482 bh 2082 """Return the dimension of the given text."""
483 bh 2074 # FIXME: find something more appropriate
484     try:
485     if self.font:
486     return (int(self.font["font-size"]),
487     len(text) * int(self.font["font-size"]))
488     else: return (12,len(text) * 10)
489     except ValueError:
490     return (12,len(text) * 10)
491    
492     def SetID(self, id):
493 bernhard 2350 """Set second part of ID stored by the svg elements.
494    
495     Will be used in make_id() as first part of an XML attribute "id".
496     Read comments at SetBaseID() and make_id().
497    
498     Common practice is to only set this to integer numbers.
499     """
500 bh 2074 self.id = id
501    
502     def SetBaseID(self, id):
503 bernhard 2350 """Set first part of ID stored by the svg elements.
504    
505     Will be used in make_id() as first part of an XML attribute "id".
506     The second part is set by SetID().
507     Check comments at make_id().
508     """
509 bh 2074 self.baseid = id
510    
511     def SetFont(self, font):
512 bh 2082 """Set the fontproperties to use with text elements."""
513 bh 2074 if font is not None:
514     fontname = font.GetFaceName()
515     size = font.GetPointSize()
516     for svgfont, pattern in fontMap.items():
517     if pattern.match(fontname):
518     fontname = svgfont
519     break
520     if fontname:
521     self.font["font-family"] = fontname
522     else: self.font["font-family"] = None
523     if size:
524     self.font["font-size"] = str(size)
525     else: self.font["font-size"] = None
526    
527     def SetPen(self, pen):
528 bh 2082 """Set the style of the pen used to draw graphics."""
529 bh 2074 if pen is TRANSPARENT_PEN:
530     self.pen = {}
531     else:
532     self.pen["stroke"] = pen.GetColor().hex()
533     self.pen["stroke-dasharray"] = join(pen.GetDashes(), ',')
534     self.pen["stroke-width"] = pen.GetWidth()
535     self.pen["stroke-linejoin"] = svg_joins[pen.GetJoin()]
536     self.pen["stroke-linecap"] = svg_caps[pen.GetCap()]
537    
538     def SetBrush(self, brush):
539 bh 2082 """Set the fill properties."""
540 bh 2074 if brush is TRANSPARENT_BRUSH:
541     self.brush['fill'] = 'none'
542     elif brush.GetPattern() is SOLID:
543     self.brush['fill'] = brush.GetColor().hex()
544     else: # TODO Handle Patterns
545     pass
546    
547     def SetTextForeground(self, color):
548 bh 2082 """Set the color of the text foreground."""
549 bh 2074 self.font['fill'] = color.hex()
550    
551     def make_style(self, line=0, fill=0, font=0):
552 bh 2082 """Build the style attribute including desired properties
553     such as fill, forground, stroke, etc."""
554 bh 2074 result = []
555     # little helper function
556     def append(pairs):
557     for key, val in pairs.items():
558     if not val in [None, '']:
559     result.append('%s:%s' % (key, val))
560     #
561     if line and len(self.pen) > 0:
562     append(self.pen)
563     if fill and len(self.brush) > 0:
564     append(self.brush)
565     if font and len(self.font) > 0:
566     append(self.font)
567     style = join(result, '; ')
568     if style:
569     return 'style="%s"' % (style, )
570     else:
571     return ''
572    
573     def make_meta(self, meta=None):
574 bh 2082 """Build the meta attribute."""
575 bh 2074 result = []
576     if not meta:
577     meta = self.meta
578     if len(meta) is 0:
579     return ''
580     for key, val in meta.items():
581     if not val in [None, '', 'none']:
582     result.append('%s:%s' % (key, val))
583     if self.flush_meta:
584     self.meta = {}
585     return 'meta="%s"' % (join(result, '; '))
586    
587     def make_id(self):
588 bernhard 2344 """Return id= string for object out of currently set baseid and id.
589    
590     Return the empty string if no id was set.
591 bernhard 2350
592     In an XML file each id should be unique
593     (see XML 1.0 section 3.3.1 Attribute Types, Validity constraint: ID)
594     So this function should only return a unique values.
595     which also coforms to the the XML "Name production" (section 3.2).
596    
597     For this it completely depends
598     on what has been set by SetBaseID() and SetID().
599     Only call this function if you have called them w unique values before
600     (or SetID(0) to get an empty result)
601    
602     A check of uniqueness in this function might be time consuming,
603     because it would require to hold and search through a complete table.
604 bernhard 2344 """
605 bh 2074 if self.id < 0:
606     return ''
607 bernhard 2344 return 'id="%s-%s"' % (self.baseid, self.id)
608 bh 2074
609     def DrawEllipse(self, x, y, dx, dy):
610 bh 2082 """Draw an ellipse."""
611 bh 2074 elips = '<ellipse cx="%s" cy="%s" rx="%s" ry="%s" %s %s %s/>\n'
612     self.write_indent(elips % (x, y, dx, dy, self.make_id(),
613     self.make_style(1,1,0), self.make_meta()) )
614    
615     def DrawCircle(self, x, y, radius):
616 bh 2082 """Draw a circle onto the virtual dc."""
617 bh 2074 self.write_indent('<circle cx="%s" cy="%s" r="%s" %s %s %s/>\n' %
618     (x, y, radius, self.make_id(), self.make_style(1,1,0),
619     self.make_meta()) )
620    
621     def DrawRectangle(self, x, y, width, height):
622 bh 2082 """Draw a rectangle with the given parameters."""
623 bh 2074 rect = '<rect x="%s" y="%s" width="%s" height="%s" %s %s %s/>\n'
624     self.write_indent(rect % ( x, y, width, height, self.make_id(),
625     self.make_style(1,1,0), self.make_meta()) )
626    
627     def DrawText(self, text, x, y):
628 bh 2082 """Draw Text at the given position."""
629 bh 2074 beginText = '<text x="%s" y="%s" %s %s %s>'
630     self.write_indent(beginText % ( x, y, self.make_id(),
631     self.make_style(0,0,1), self.make_meta()) )
632     self.file.write(escape(text))
633     self.file.write('</text>\n')
634    
635     def DrawLines(self, points):
636 bh 2082 """Draw some points into a Buffer that will be
637 bh 2074 written before the next object.
638 bh 2082 """
639 bh 2074 self.DrawPolygon(points,0)
640    
641     def DrawPolygon(self, polygon, closed=1):
642 bh 2082 """Draw a polygon onto the virtual dc."""
643 bh 2074 self.write_indent('<path %s ' % (self.make_style(1,1,0)))
644     data = []
645     i = 0
646     for point in polygon:
647     if i is 0:
648     data.append('M %s %s ' % (point.x, point.y))
649     else:
650     data.append('L %s %s ' % (point.x, point.y))
651     i+=1
652     # FIXME: Determine if path is closed
653     if closed:
654     data.append('Z')
655     # Put everything together and write it to the file
656     self.file.write('%s %s d="%s"/>\n' % (self.make_id(),
657     self.make_meta(), join(data, '') ) )
658    
659     def DrawSpline(self, points, closed=0):
660 bh 2082 """Draw a spline object.
661     """
662 bh 2074 self.DrawPolygon(points, 0)
663     print "TODO: DrawSpline(..)"
664     return # TODO: Implement
665    
666     def BeginClipPath(self, id):
667 bh 2082 """Build a clipping region to draw in."""
668 bh 2074 self.write_indent('<clipPath id="%s">\n' % id)
669     self.indent_level += 1
670    
671     def EndClipPath(self):
672 bh 2082 """End a clip path."""
673 bh 2074 self.indent_level -= 1
674     self.write_indent("</clipPath>\n")

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26