1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6import os 

7import base64 

8 

9import numpy as num 

10 

11from pyrocko import automap 

12from pyrocko.guts import String, Float, StringChoice 

13from pyrocko.plot import AutoScaler, AutoScaleMode 

14from pyrocko.dataset import topo 

15 

16from pyrocko.gui.talkie import TalkieRoot, TalkieConnectionOwner 

17from pyrocko.gui.qt_compat import qc, qw 

18from pyrocko.gui.vtk_util import cpt_to_vtk_lookuptable 

19 

20from .. import common 

21from ..state import \ 

22 state_bind_combobox, state_bind 

23 

24 

25def random_id(): 

26 return base64.urlsafe_b64encode(os.urandom(16)).decode('ascii') 

27 

28 

29class ElementState(TalkieRoot): 

30 

31 element_id = String.T() 

32 

33 def __init__(self, **kwargs): 

34 if 'element_id' not in kwargs: 

35 kwargs['element_id'] = random_id() 

36 

37 TalkieRoot.__init__(self, **kwargs) 

38 

39 

40class Element(TalkieConnectionOwner): 

41 def __init__(self): 

42 TalkieConnectionOwner.__init__(self) 

43 self._parent = None 

44 self._state = None 

45 

46 def remove(self): 

47 if self._parent and self._state: 

48 self._parent.state.elements.remove(self._state) 

49 

50 def set_parent(self, parent): 

51 self._parent = parent 

52 

53 def unset_parent(self): 

54 print(self) 

55 raise NotImplementedError 

56 

57 def bind_state(self, state): 

58 self._state = state 

59 

60 def unbind_state(self): 

61 self.talkie_disconnect_all() 

62 self._state = None 

63 

64 def update_visibility(self, visible): 

65 self._state.visible = visible 

66 

67 def get_title_control_remove(self): 

68 button = common.MyDockWidgetTitleBarButton('\u00d7') 

69 button.setStatusTip('Remove Element') 

70 button.clicked.connect(self.remove) 

71 return button 

72 

73 def get_title_control_visible(self): 

74 assert hasattr(self._state, 'visible') 

75 

76 button = common.MyDockWidgetTitleBarButtonToggle('\u2b53', '\u2b54') 

77 button.setStatusTip('Toggle Element Visibility') 

78 button.toggled.connect(self.update_visibility) 

79 

80 def set_button_checked(*args): 

81 button.blockSignals(True) 

82 button.set_checked(self._state.visible) 

83 button.blockSignals(False) 

84 

85 set_button_checked() 

86 

87 self.talkie_connect( 

88 self._state, 'visible', set_button_checked) 

89 

90 return button 

91 

92 

93class CPTChoice(StringChoice): 

94 choices = [ 

95 'slip_colors', 'seismic', 'seismic_r', 'jet', 'hot_r', 'gist_earth_r'] 

96 

97 

98class CPTState(ElementState): 

99 cpt_name = String.T(default=CPTChoice.choices[0]) 

100 cpt_mode = String.T(default=AutoScaleMode.choices[1]) 

101 cpt_scale_min = Float.T(optional=True) 

102 cpt_scale_max = Float.T(optional=True) 

103 

104 

105class CPTHandler(Element): 

106 

107 def __init__(self): 

108 

109 Element.__init__(self) 

110 self._cpts = {} 

111 self._autoscaler = None 

112 self._lookuptable = None 

113 self._cpt_combobox = None 

114 self._values = None 

115 self._state = None 

116 self._cpt_scale_lineedit = None 

117 

118 def bind_state(self, cpt_state, update_function): 

119 for state_attr in [ 

120 'cpt_name', 'cpt_mode', 'cpt_scale_min', 'cpt_scale_max']: 

121 

122 self.talkie_connect( 

123 cpt_state, state_attr, update_function) 

124 

125 self._state = cpt_state 

126 

127 def unbind_state(self): 

128 Element.unbind_state(self) 

129 self._cpts = {} 

130 self._lookuptable = None 

131 self._values = None 

132 self._autoscaler = None 

133 

134 def open_cpt_load_dialog(self): 

135 caption = 'Select one *.cpt file to open' 

136 

137 fns, _ = qw.QFileDialog.getOpenFileNames( 

138 self._parent, caption, options=common.qfiledialog_options) 

139 

140 if fns: 

141 self.load_cpt_file(fns[0]) 

142 

143 def load_cpt_file(self, path): 

144 cpt_name = 'USR' + os.path.basename(path).split('.')[0] 

145 self._cpts.update([(cpt_name, automap.read_cpt(path))]) 

146 

147 self._state.cpt_name = cpt_name 

148 

149 self._update_cpt_combobox() 

150 self.update_cpt() 

151 

152 def _update_cpt_combobox(self): 

153 from pyrocko import config 

154 conf = config.config() 

155 

156 if self._cpt_combobox is None: 

157 raise ValueError('CPT combobox needs init before updating!') 

158 

159 cb = self._cpt_combobox 

160 

161 if cb is not None: 

162 cb.clear() 

163 

164 for s in CPTChoice.choices: 

165 if s not in self._cpts: 

166 try: 

167 cpt = automap.read_cpt(topo.cpt(s)) 

168 except Exception: 

169 from matplotlib import pyplot as plt 

170 cmap = plt.cm.get_cmap(s) 

171 cpt = automap.CPT.from_numpy(cmap(range(256))[:, :-1]) 

172 

173 self._cpts.update([(s, cpt)]) 

174 

175 cpt_dir = conf.colortables_dir 

176 if os.path.isdir(cpt_dir): 

177 for f in [ 

178 f for f in os.listdir(cpt_dir) 

179 if f.lower().endswith('.cpt')]: 

180 

181 s = 'USR' + os.path.basename(f).split('.')[0] 

182 self._cpts.update( 

183 [(s, automap.read_cpt(os.path.join(cpt_dir, f)))]) 

184 

185 for i, (s, cpt) in enumerate(self._cpts.items()): 

186 cb.insertItem(i, s, qc.QVariant(self._cpts[s])) 

187 cb.setItemData(i, qc.QVariant(s), qc.Qt.ToolTipRole) 

188 

189 cb.setCurrentIndex(cb.findText(self._state.cpt_name)) 

190 

191 def _update_cptscale_lineedit(self): 

192 le = self._cpt_scale_lineedit 

193 if le is not None: 

194 le.clear() 

195 

196 self._cptscale_to_lineedit(self._state, le) 

197 

198 def _cptscale_to_lineedit(self, state, widget): 

199 # sel = widget.selectedText() == widget.text() 

200 

201 crange = (None, None) 

202 if self._lookuptable is not None: 

203 crange = self._lookuptable.GetRange() 

204 

205 if state.cpt_scale_min is not None and state.cpt_scale_max is not None: 

206 crange = state.cpt_scale_min, state.cpt_scale_max 

207 

208 fmt = ', '.join(['%s' if item is None else '%g' for item in crange]) 

209 

210 widget.setText(fmt % crange) 

211 

212 # if sel: 

213 # widget.selectAll() 

214 

215 def update_cpt(self): 

216 state = self._state 

217 

218 if self._autoscaler is None: 

219 self._autoscaler = AutoScaler() 

220 

221 if self._cpt_scale_lineedit: 

222 if state.cpt_mode == 'off': 

223 self._cpt_scale_lineedit.setEnabled(True) 

224 else: 

225 self._cpt_scale_lineedit.setEnabled(False) 

226 

227 if state.cpt_scale_min is not None: 

228 state.cpt_scale_min = None 

229 

230 if state.cpt_scale_max is not None: 

231 state.cpt_scale_max = None 

232 

233 if state.cpt_name is not None and self._values is not None: 

234 vscale = (num.nanmin(self._values), num.nanmax(self._values)) 

235 

236 vmin, vmax = None, None 

237 if None not in (state.cpt_scale_min, state.cpt_scale_max): 

238 vmin, vmax = state.cpt_scale_min, state.cpt_scale_max 

239 else: 

240 vmin, vmax, _ = self._autoscaler.make_scale( 

241 vscale, override_mode=state.cpt_mode) 

242 

243 self._cpts[state.cpt_name].scale(vmin, vmax) 

244 cpt = self._cpts[state.cpt_name] 

245 

246 vtk_lut = cpt_to_vtk_lookuptable(cpt) 

247 vtk_lut.SetNanColor(0.0, 0.0, 0.0, 0.0) 

248 

249 self._lookuptable = vtk_lut 

250 self._update_cptscale_lineedit() 

251 

252 elif state.cpt_name and self._values is None: 

253 raise ValueError('No values passed to colormapper!') 

254 

255 def cpt_controls(self, parent, state, layout): 

256 self._parent = parent 

257 

258 iy = layout.rowCount() + 1 

259 

260 layout.addWidget(qw.QLabel('Color Map'), iy, 0) 

261 

262 cb = common.CPTComboBox() 

263 layout.addWidget(cb, iy, 1) 

264 state_bind_combobox( 

265 self, state, 'cpt_name', cb) 

266 

267 self._cpt_combobox = cb 

268 

269 pb = qw.QPushButton('Load CPT') 

270 layout.addWidget(pb, iy, 2) 

271 pb.clicked.connect(self.open_cpt_load_dialog) 

272 

273 iy += 1 

274 layout.addWidget(qw.QLabel('Color Scaling'), iy, 0) 

275 

276 cb = common.string_choices_to_combobox(AutoScaleMode) 

277 layout.addWidget(cb, iy, 1) 

278 state_bind_combobox( 

279 self, state, 'cpt_mode', cb) 

280 

281 le = qw.QLineEdit() 

282 le.setEnabled(False) 

283 layout.addWidget(le, iy, 2) 

284 state_bind( 

285 self, state, 

286 ['cpt_scale_min', 'cpt_scale_max'], _lineedit_to_cptscale, 

287 le, [le.editingFinished, le.returnPressed], 

288 self._cptscale_to_lineedit) 

289 

290 self._cpt_scale_lineedit = le 

291 

292 

293def _lineedit_to_cptscale(widget, cpt_state): 

294 s = str(widget.text()) 

295 s = s.replace(',', ' ') 

296 

297 crange = tuple((float(i) for i in s.split())) 

298 crange = tuple(( 

299 crange[0], 

300 crange[0]+0.01 if crange[0] >= crange[1] else crange[1])) 

301 

302 try: 

303 cpt_state.cpt_scale_min, cpt_state.cpt_scale_max = crange 

304 except Exception: 

305 raise ValueError( 

306 'need two numerical values: <vmin>, <vmax>') 

307 

308 

309__all__ = [ 

310 'Element', 

311 'ElementState', 

312 'random_id', 

313]