1
2
3 """Handles interactions with ds9 (U{http://hea-www.harvard.edu/RD/ds9/}).
4 Ds9 is an image display and analysis tool commonly used in astrophysics,
5 but it's good for any kind of greyscale image.
6
7 Note: there is an extra, special property Coordsys. This is not fed to
8 ds9. It is merely something that you can use to remember what coordinate
9 system the regions are in.
10 """
11
12
13 import re
14 import threading
15 import os
16 import time
17 import copy as COPY
18 from gmisclib import die
19 from gmisclib import g_exec
20
21 COLORS = ('white', 'black', 'red', 'green', 'blue', 'cyan', 'magenta', 'yellow')
22 PLOTSYMS = ('circle', 'diamond', 'plus', 'cross')
23
27
28
31 self.t = threading.Thread(target=os.system, args=(s,),
32 name='execwait: %s' % s
33 )
34 self.t.start()
35
38
39
41 - def __init__(self, ds9start=None, xpaname='ds9',
42 xpaset=['xpaset', ],
43 xpaset_p=['xpaset', '-p'],
44 xpaget=['xpaget']
45 ):
46 if ds9start is None:
47 ds9start = 'ds9'
48 self.setap = tuple(xpaset_p) + (xpaname,)
49 self.seta = tuple(xpaset) + (xpaname,)
50 self.geta = tuple(xpaget) + (xpaname,)
51 self.ds9ew = None
52 self.debug = 0
53
54 try:
55 tmp = self.getall('about')
56 if tmp:
57 self.closed = False
58 return
59 except g_exec.ExecError:
60 pass
61
62 self.ds9ew = execwait(ds9start)
63 sleep = 0.10
64 time.sleep(2*sleep)
65 while True:
66 try:
67 tmp = self.getall('about')
68 if tmp:
69 self.closed = False
70 return
71 except g_exec.ExecError:
72 sleep += 0.1
73 time.sleep(sleep)
74
75
76
77 - def set(self, *args):
78 tmp = self.setap + tuple(args)
79 g_exec.getall(tmp[0], tmp)
80
81
95
96
98 return self.getall(*args)[-1]
99
100
102 return list(self.getiter(*args))
103
104
106 tmp = self.geta + tuple(args)
107 for q in g_exec.getiter_raw(tmp[0], tmp):
108 yield q.rstrip()
109
110
112 if self.ds9ew and not self.closed:
113 self.set('quit')
114 self.ds9ew.wait()
115 self.ds9ew = None
116 self.closed = True
117
118
126
127
131
132
135 self._props = kwargs
136 self._pshared = True
137 self.Coordsys = coordsys
138
140 raise RuntimeError, 'Virtual function'
141
143 if self._pshared:
144 self._props = self._props.copy()
145 self._pshared = False
146 self._props[k] = v
147
150
152 return self._props.get(*args)
153
154
156 o = [ '#' ]
157 for (k, v) in self._props.items():
158 try:
159 pof = PROPout[k]
160 except KeyError, x:
161 raise UnknownProperty( *(x.args) )
162 o.extend( pof(k, v) )
163 if len(o) > 1:
164 return ' '.join(o)
165 return ''
166
167
169 if self._pshared:
170 self._props = self._props.copy()
171 self._pshared = False
172 self._props.update(kwargs)
173 return self
174
176 return (self.Coordsys is None or coordsys is None
177 or self.Coordsys == coordsys)
178
179
180
181 -def copy(r, **kwargs):
182 """Copy a region and update some of the keyword
183 arguments (e.g. color). The original object is unchanged,
184 and you can copy any subclass of Region.
185 """
186 assert isinstance(r, Region)
187 tmp = COPY.copy(r)
188 tmp.chgprops(**kwargs)
189 return tmp
190
191
193 - def __init__(self, x0, y0, x1, y1, Coordsys=None, **kwargs):
194 Region.__init__(self, kwargs, Coordsys)
195 self.x0 = x0
196 self.y0 = y0
197 self.x1 = x1
198 self.y1 = y1
199
201 return 'line %10g %10g %10g %10g %s' % ( self.x0+1, self.y0+1,
202 self.x1+1, self.y1+1, self.props() )
203
204 - def end(self, whichend):
205 if whichend == 0:
206 return point(self.x0, self.y0, **self._props)
207 elif whichend == 1:
208 return point(self.x1, self.y1, **self._props)
209 else:
210 raise ValueError, "Whichend == 0 or 1"
211
213 return point(0.5*(self.x0+self.x1), 0.5*(self.y0+self.y1), Coordsys=self.Coordsys)
214
215
217 - def __init__(self, x, y, r, Coordsys=None, **kwargs):
222
224 return 'circle %10g %10g %10g %s' % ( self.x+1, self.y+1, self.r, self.props() )
225
227 return point(self.x, self.y, Coordsys=self.Coordsys)
228
229
231 - def __init__(self, x, y, Coordsys=None, **kwargs):
235
237 return 'point %10g %10g %s' % ( self.x+1, self.y+1, self.props() )
238
239
240
241
243 - def __init__(self, x, y, text, Coordsys=None, **kwargs):
244 Region.__init__(self, kwargs, Coordsys)
245 self.addprop('text', text)
246 self.x = x
247 self.y = y
248 self.text = text
249
251 return 'text %10g %10g %s' % ( self.x+1, self.y+1, self.props() )
252
253
255 if s.startswith('{') and s.endswith('}'):
256 return s[1:-1]
257 elif len(s)>1 and s.startswith('"') and s.endswith('"'):
258 return s[1:-1]
259 elif len(s)>1 and s.startswith("'") and s.endswith("'"):
260 return s[1:-1]
261 return s
262
263
264
265 PROPin = {'color': str, 'text': dequote, 'font': dequote,
266 'select': int, 'edit': int, 'move': int, 'rotate': int,
267 'delete': int, 'fixed': int,
268 'line': None, 'tag': None,
269 'ruler': int, 'width': int,
270 'point': str, 'highlite': int, 'include': int
271 }
272
273
275 return [ '%s={%s}' % (k, v) ]
276
277
279 return [ '%s=%d' % (k, v) ]
280
282 return [ '%s=%s' % (k, v) ]
283
286
288 return [ '%s = %d %d' % (k, v[0], v[1]) ]
289
290 _oktag = re.compile("^[a-zA-Z][a-zA-Z0-9]*$")
296
298 raise RuntimeError, "'tags', not 'tag'."
299
300 PROPout = {'color': _kvstr, 'text': _kvquote, 'font': _kvquote,
301 'select': _kvint, 'edit': _kvint, 'move': _kvint, 'rotate': _kvint,
302 'delete': _kvint, 'fixed': _kvint,
303 'line': _kvline, 'tags': _kvtags, 'tag': _kverr,
304 'ruler': _kvint, 'width': _kvint,
305 'point': _kvstr, 'highlite': _kvint, 'include': _kvint
306 }
307
308
309
310 _rparse = re.compile(r"""{[^}]*}|"[^"]*"|'[^']'|[^\s,()=#]+|[#]|=""")
311
314
315
316
318 kwargs = {}
319 i = 0
320 while i+2 < len(ap):
321 arg = ap[i]
322 if arg in PROPin and ap[i+1] == '=':
323 if arg == 'line' and i+3 < len(ap):
324 kwargs[arg] = ( int(ap[i+2]), int(ap[i+3]) )
325 i += 4
326 elif arg == 'tag':
327 if 'tags' not in kwargs:
328
329
330 kwargs['tags'] = [ dequote(ap[i+2]) ]
331 else:
332 kwargs['tags'].append( dequote(ap[i+2]) )
333 i += 3
334 else:
335 kwargs[arg] = PROPin[arg]( ap[i+2] )
336 i += 3
337 else:
338 die.info('ap=%s' % str(ap))
339 die.die('Unexpected word in property: %s' % arg)
340
341 return kwargs
342
343
345 print "TPP"
346 pp = property_parser
347 assert pp(['select', '=', '1']) == {'select': 1}
348 assert pp(['text', '=', '1']) == {'text': '1'}
349 assert pp(['text', '=', '{foo}']) == {'text': 'foo'}
350 assert pp(['text', '=', '"foo"']) == {'text': 'foo'}
351 assert pp(['line', '=', '1', '1']) == {'line': (1,1)}
352 assert pp(['select', '=', '1', 'fixed', '=', '0']) == {'select': 1, 'fixed': 0}
353
354
356 s = s.strip()
357 kwargs = defprop.copy()
358 if s.startswith('-'):
359 kwargs['include'] = 0
360 s = s[1:]
361 elif s.startswith('+'):
362 kwargs['include'] = 1
363 s = s[1:]
364
365
366 aa = make_chunks(s)
367 x = aa[0]
368
369 if '#' in aa:
370 hi = aa.index('#')
371
372 ap = aa[hi+1:]
373 aa = aa[1:hi]
374 else:
375 aa = aa[1:]
376 ap = []
377
378
379 args = [ float(q) for q in aa ]
380 kwargs.update( property_parser(ap) )
381 return (x, args, kwargs)
382
383
385 print "TRP"
386 x, args, kwargs = region_parser('line(0,0,1,2)', {})
387 assert x == 'line'
388 assert args == [0, 0, 1, 2]
389 assert kwargs == {}
390
391 x, args, kwargs = region_parser('+point(-1,2)', {})
392 assert x == 'point'
393 assert args == [-1, 2]
394 assert kwargs == {'include': 1}
395
396 x, args, kwargs = region_parser('-point(-1,2) # color=red', {})
397 assert x == 'point'
398 assert args == [-1, 2]
399 assert kwargs == {'include': 0, 'color': 'red'}
400
401 x, args, kwargs = region_parser('circle(0,2,3) # text={foo}', {})
402 assert x == 'circle'
403 assert args == [0,2,3]
404 assert kwargs == {'text': 'foo'}
405
406 x, args, kwargs = region_parser('circle(0,2,3) # text="foo" rotate =1', {})
407 assert x == 'circle'
408 assert args == [0,2,3]
409 assert kwargs == {'text': 'foo', 'rotate': 1}
410
411 x, args, kwargs = region_parser('circle(0,2,3) # text="foo bar" rotate = 0 fixed= 1', {})
412 assert x == 'circle'
413 assert args == [0,2,3]
414 assert kwargs == {'text': 'foo bar', 'rotate': 0, 'fixed': 1}
415
416 x, args, kwargs = region_parser('circle(0,2,3) # text={foo bar} line=1 1 fixed=0', {})
417 assert x == 'circle'
418 assert args == [0,2,3]
419 assert kwargs == {'text': 'foo bar', 'line': (1,1), 'fixed': 0}
420
421
422
423 _text_region = re.compile(r'\s*#\s*text[( \t]([0-9eE.+-]+)[, \t]+([0-9eE.+-]+)[) \t]\s*(.*)$')
424
426 m = _text_region.match(s)
427 assert m
428 kwargs = property_parser(make_chunks(m.group(3)))
429
430 txt = kwargs.pop('text')
431 return text(float(m.group(1)), float(m.group(2)), txt, **kwargs)
432
434 print "TTK"
435 tmp = text_kluge('# text(2,1) text={fred} color=red tag={RefImgF}')
436 assert tmp.getprop('color') == 'red'
437 assert tmp.getprop('text') == 'fred'
438 assert tmp.getprop('tags') == ['RefImgF',]
439 assert abs(tmp.x-2.0)<0.0001 and abs(tmp.y-1.0)<0.00001
440
441
443 gp = {'include': 1, 'text': '', 'color': 'green',
444 'font': 'helvetica 10 normal', 'edit': 1,
445 'move': 1, 'delete': 1, 'fixed': 0
446 }
447 for s in slist:
448 s = s.strip()
449
450 if _text_region.match(s):
451 yield text_kluge(s)
452 continue
453 elif s.startswith('#'):
454
455 continue
456 if s.startswith('global'):
457 gp = property_parser(make_chunks(s[len('global'):]))
458 continue
459 x, args, kwargs = region_parser(s, gp)
460 if x is None:
461 pass
462 elif x == 'physical':
463 pass
464 elif x == 'image':
465 pass
466 elif x == 'circle':
467 assert len(args) == 3
468 yield circle(args[0]-1, args[1]-1, args[2], Coordsys=Coordsys, **kwargs)
469 elif x == 'line':
470 assert len(args) == 4
471 yield line(args[0]-1, args[1]-1,
472 args[2]-1, args[3]-1, Coordsys=Coordsys, **kwargs)
473 elif x == 'point':
474 assert len(args) == 2
475 yield point(args[0]-1, args[1]-1, Coordsys=Coordsys, **kwargs)
476 elif x == 'text':
477 if len(args) == 2:
478
479
480
481 yield text(args[0]-1, args[1]-1, Coordsys=Coordsys, **kwargs)
482 elif len(args) == 3:
483 yield text(args[0]-1, args[1]-1, args[2], Coordsys=Coordsys, **kwargs)
484 else:
485 die.die('Unsupported region: %s in <%s>' % (x, s))
486
487
489 """Read regions from a file. This is an iterator.
490 Fd is a file descriptor.
491 """
492 return r_list_factory(fd, Coordsys)
493
494
496 """Read regions from a file.
497 Fd is a file descriptor.
498 """
499 return list(r_list_factory(fd, Coordsys))
500
501
502 PLOTSTYLES = ('discrete', 'line', 'step', 'quadratic', 'errorbar')
503
504
538
539
540
541
544 ds9_io.__init__(self, ds9start=ds9start)
545 self.set('mode', 'pointer')
546 self.frame = -1
547
548 - def load(self, fn):
549 self.set('regions', 'delete', 'all')
550 self.set('file', fn)
551
553 return self.getlast( 'iis', 'filename' )
554
556 tmp = self.getlast( 'frame', 'all' ).split()
557 if 'XPA$END' in tmp:
558
559
560
561 return []
562 return tmp
563
565 """A catch-all command for anything that's not otherwise implemented."""
566 self.set('frame', *cmds)
567
568
570 """Note: deleting all the frames crashes ds9.
571 """
572 for i in framelist:
573 self.select_frame(i)
574 self.frame_cmd('delete')
575
576
578 """Select a frame on the attached ds9 image display.
579 The frame will be created if it didn't already exist.
580 @param i: Which frame? Apparently, any integer is OK,
581 even negative.
582 @type i: L{int}
583 """
584 si = str(int(i))
585 if si != self.frame:
586 self.set('frame', 'frameno', si)
587 self.frame = si
588
589
595
598
600 self.set('regions', 'delete', 'all')
601
603 for g in group:
604 self.set('regions', 'group', g, 'delete')
605
607 """Add a list of regions to the current frame.
608 @param region: one or more regions.
609 @type region: C{[ region ]}, where C{region} is normally
610 an instance of L{Region}, but can be anything that
611 produces a string suitable for X{ds9}.
612 See C{http://hea-www.harvard.edu/RD/ds9/ref/xpa.html#regions}.
613 """
614
615 self.set_with_input([ str(r) for r in region ], 'regions')
616
617
619 """A catch-all command for anything that's not otherwise implemented.
620 This does C{xpaset -p}, so you cannot use it for region definitions
621 that need multi-line input via L{set_with_input}.
622 """
623 self.set('regions', *cmds)
624
626 self.set('zoom', 'to', str(z))
627
629 """A catch-all command for anything that's not otherwise implemented."""
630 self.set('scale', *cmds)
631
633 """A catch-all command for anything that's not otherwise implemented."""
634 self.set('cmap', *cmds)
635
637 assert m in ['tile', 'blink', 'single']
638 if m == 'tile':
639 self.set('tile', 'yes')
640 else:
641 self.set('tile', 'no')
642 if m == 'single':
643 self.set('single')
644 if m == 'blink':
645 self.set('blink')
646
647 - def pan_to(self, x, y, coordsys):
648 self.set('pan', 'to', str(x), str(y), coordsys)
649
650 plotname_ok = re.compile('[a-z][a-zA-Z_]*')
651
652 - def plot(self, name, xylist, title='', xlabel='', ylabel='',
653 format='(x,y)', line='solid',
654 color='black', symbol=None):
655 assert name != 'new'
656 assert self.plotname_ok.match(name), "Bad plot name: %s" % name
657 ds9fmt, formatted = _format_plot(xylist, format)
658 if name in self.list_plots():
659 assert title==''
660 assert xlabel==''
661 assert ylabel==''
662 self.set_with_input( formatted, 'plot %s data %s' % (name, ds9fmt))
663 else:
664 self.set_with_input( formatted,
665 'plot new name %s {%s} {%s} {%s} %s'
666 % (name, title, xlabel, ylabel, ds9fmt)
667 )
668 assert color in COLORS
669 self.set('plot', 'view', 'discrete', ['yes', 'no'][symbol is None])
670 self.set('plot', 'view', 'line', ['yes', 'no'][line is None])
671 if symbol is not None:
672 assert symbol in PLOTSYMS
673 self.set('plot', 'color', 'discrete', color)
674 self.set('plot', 'line', 'discrete', symbol)
675 if line is not None:
676 self.set('plot', 'color', 'line', color)
677 self.set('plot', 'line', 'dash', ['no', 'yes'][line=='solid'])
678
680 self.set('plot', 'close', '{%s}' % name)
681
684
687
690
691
693 """This is just a convenient way to write regions to a file,
694 instead of writing them to a ds9 session. Note that plots
695 and other interactive things are simply no-ops.
696 """
697
699 self.fd = fd
700 self.frame = -1
701 self.fd.writelines('# Region file format: DS9 version 4.0\n')
702
704 si = str(i)
705 if si != self.frame:
706 self.fd.writelines('# frame %s\n' % i)
707 self.frame = si
708
709
711 self.fd.writelines( [ str(region), '\n'] )
712
713
716
718 return self.fd.flush()
719
720 - def plot(self, name, *args, **kwargs):
722
723
726
730
731
742
743
745 print "TPL"
746 import math
747 xylist1 = [ (x, math.cos(x*0.3)) for x in range(100) ]
748 xylist2 = [ (x, math.sin(x*0.33)) for x in range(100) ]
749 tmp = ds9()
750 tmp.plot('some_data', xylist1, 'Some Data', 'x-axis', 'y-axis')
751 tmp.plot('some_data', xylist2, color='red')
752 time.sleep(6)
753
754
755
756 if __name__ == '__main__':
757 import sys
758 if len(sys.argv) == 2:
759 for r in read_regions_iter(open(sys.argv[1], 'r')):
760 print r
761 sys.exit(0)
762 test_property_parser()
763 test_text_kluge()
764 test_region_parser()
765 test_io()
766 test_tags()
767 test_plot()
768