1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6import math 

7import numpy as num 

8 

9from pyrocko.guts import Bool, Float, StringChoice 

10from pyrocko import cake, automap, plot 

11from pyrocko.dataset import topo 

12from pyrocko.gui.qt_compat import qw, qc 

13 

14from pyrocko.gui.vtk_util import TrimeshPipe, faces_to_cells, \ 

15 cpt_to_vtk_lookuptable 

16 

17from .base import Element, ElementState 

18from .. import state as vstate 

19from pyrocko import geometry 

20 

21from .. import common 

22 

23guts_prefix = 'sparrow' 

24 

25 

26def ticks(vmin, vmax, vstep): 

27 vmin = num.floor(vmin / vstep) * vstep 

28 vmax = num.ceil(vmax / vstep) * vstep 

29 n = int(round((vmax - vmin) / vstep)) 

30 return vmin + num.arange(n+1) * vstep 

31 

32 

33def nice_value_circle(step): 

34 step = plot.nice_value(step) 

35 if step > 30.: 

36 return 30. 

37 

38 return step 

39 

40 

41class TopoMeshPipe(TrimeshPipe): 

42 

43 def __init__( 

44 self, tile, cells_cache=None, 

45 mask_ocean=False, mask_land=False, **kwargs): 

46 

47 vertices, faces = geometry.topo_to_mesh( 

48 tile.y(), tile.x(), tile.data, 

49 cake.earthradius) 

50 

51 self._exaggeration = 1.0 

52 self._tile = tile 

53 self._raw_vertices = vertices 

54 

55 centers = geometry.face_centers(vertices, faces) 

56 

57 altitudes = (geometry.vnorm(centers) - 1.0) * cake.earthradius 

58 if mask_ocean: 

59 mask = num.all(tile.data.flatten()[faces] == 0, axis=1) 

60 altitudes[mask] = None 

61 

62 if mask_land: 

63 mask = num.all(tile.data.flatten()[faces] >= 0, axis=1) 

64 altitudes[mask] = None 

65 

66 if cells_cache is not None: 

67 if id(faces) not in cells_cache: 

68 cells_cache[id(faces)] = faces_to_cells(faces) 

69 

70 cells = cells_cache[id(faces)] 

71 else: 

72 cells = faces_to_cells(faces) 

73 

74 TrimeshPipe.__init__( 

75 self, self._raw_vertices, cells=cells, values=altitudes, **kwargs) 

76 

77 def set_exaggeration(self, exaggeration): 

78 if self._exaggeration != exaggeration: 

79 factors = \ 

80 (cake.earthradius + self._tile.data.flatten()*exaggeration) \ 

81 / (cake.earthradius + self._tile.data.flatten()) 

82 self.set_vertices(self._raw_vertices * factors[:, num.newaxis]) 

83 self._exaggeration = exaggeration 

84 

85 

86class TopoCPTChoice(StringChoice): 

87 choices = ['light', 'uniform'] 

88 

89 

90class TopoState(ElementState): 

91 visible = Bool.T(default=True) 

92 exaggeration = Float.T(default=1.0) 

93 opacity = Float.T(default=1.0) 

94 smooth = Bool.T(default=False) 

95 cpt = TopoCPTChoice.T(default='light') 

96 shading = vstate.ShadingChoice.T(default='phong') 

97 resolution_max_factor = Float.T(default=1.0) 

98 resolution_min_factor = Float.T(default=1.0) 

99 coverage_factor = Float.T(default=1.0) 

100 

101 def create(self): 

102 element = TopoElement() 

103 return element 

104 

105 

106class TopoElement(Element): 

107 

108 def __init__(self): 

109 Element.__init__(self) 

110 self._parent = None 

111 self.mesh = None 

112 self._controls = None 

113 

114 # region = (-180., 180, -90, 90) 

115 # self._tile = topo.get('ETOPO1_D8', region) 

116 

117 self._visible = False 

118 self._active_meshes = {} 

119 self._meshes = {} 

120 self._cells = {} 

121 self._cpt_name = None 

122 self._lookuptables = {} 

123 

124 def get_name(self): 

125 return 'Topography' 

126 

127 def bind_state(self, state): 

128 Element.bind_state(self, state) 

129 for var in ['visible', 'exaggeration', 'opacity', 'smooth', 'shading', 

130 'cpt', 'resolution_min_factor', 'resolution_max_factor', 

131 'coverage_factor']: 

132 self.register_state_listener3(self.update, state, var) 

133 

134 self._state = state 

135 

136 def unbind_state(self): 

137 self._listeners.clear() 

138 

139 def set_parent(self, parent): 

140 self._parent = parent 

141 

142 for var in ['distance', 'lat', 'lon']: 

143 self.register_state_listener3( 

144 self.update, self._parent.state, var) 

145 

146 self._parent.add_panel( 

147 self.get_name(), 

148 self._get_controls(), 

149 visible=True, 

150 title_controls=[ 

151 self.get_title_control_remove(), 

152 self.get_title_control_visible()]) 

153 

154 self.update() 

155 

156 def unset_parent(self): 

157 self.unbind_state() 

158 if self._parent: 

159 for k in self._active_meshes: 

160 self._parent.remove_actor(self._active_meshes[k].actor) 

161 

162 self._active_meshes.clear() 

163 self._meshes.clear() 

164 self._cells.clear() 

165 

166 if self._controls: 

167 self._parent.remove_panel(self._controls) 

168 self._controls = None 

169 

170 self._parent.update_view() 

171 self._parent = None 

172 

173 def select_dems(self, delta, region): 

174 if not self._parent: 

175 return [], [] 

176 

177 _, size_y = self._parent.renwin.GetSize() 

178 

179 dmin = 2.0 * delta * 1.0 / float(size_y) \ 

180 / self._state.resolution_max_factor # [deg] 

181 dmax = 2.0 * delta * 20.0 \ 

182 * self._state.resolution_min_factor / float(size_y) 

183 

184 result = [ 

185 topo.select_dem_names( 

186 k, dmin, dmax, topo.positive_region(region), mode='highest') 

187 

188 for k in ['ocean', 'land']] 

189 

190 if not any(result) and delta > 20.: 

191 return ['ETOPO1_D8'], ['ETOPO1_D8'] 

192 else: 

193 return result 

194 

195 def update_cpt(self, cpt_name): 

196 if cpt_name not in self._lookuptables: 

197 if cpt_name == 'light': 

198 topo_cpt_wet = 'light_sea' 

199 topo_cpt_dry = 'light_land' 

200 

201 elif cpt_name == 'uniform': 

202 topo_cpt_wet = 'light_sea_uniform' 

203 topo_cpt_dry = 'light_land_uniform' 

204 

205 cpt_wet = automap.read_cpt(topo.cpt(topo_cpt_wet)) 

206 cpt_dry = automap.read_cpt(topo.cpt(topo_cpt_dry)) 

207 cpt_combi = automap.cpt_merge_wet_dry(cpt_wet, cpt_dry) 

208 

209 lut_combi = cpt_to_vtk_lookuptable(cpt_combi) 

210 lut_combi.SetNanColor(0.0, 0.0, 0.0, 0.0) 

211 

212 self._lookuptables[cpt_name] = lut_combi 

213 

214 def update(self, *args): 

215 

216 pstate = self._parent.state 

217 delta = pstate.distance * geometry.r2d * 0.5 

218 visible = self._state.visible 

219 

220 self.update_cpt(self._state.cpt) 

221 

222 step = nice_value_circle( 

223 max(1./8., min(2**round(math.log(delta) / math.log(2.)), 10.))) 

224 

225 lat_min, lat_max, lon_min, lon_max, lon_closed = common.cover_region( 

226 pstate.lat, pstate.lon, delta*self._state.coverage_factor, step) 

227 

228 if lon_closed: 

229 lon_max = 180. 

230 

231 region = lon_min, lon_max, lat_min, lat_max 

232 

233 dems_ocean, dems_land = self.select_dems(delta, region) 

234 

235 lat_majors = ticks(lat_min, lat_max-step, step) 

236 lon_majors = ticks(lon_min, lon_max-step, step) 

237 

238 wanted = set() 

239 if visible: 

240 for ilat, lat in enumerate(lat_majors): 

241 for ilon, lon in enumerate(lon_majors): 

242 lon = ((lon + 180.) % 360.) - 180. 

243 

244 region = topo.positive_region( 

245 (lon, lon+step, lat, lat+step)) 

246 

247 for demname in dems_land[:1] + dems_ocean[:1]: 

248 mask_ocean = demname.startswith('SRTM') \ 

249 or demname.startswith('Iceland') 

250 

251 mask_land = demname.startswith('ETOPO') \ 

252 and (dems_land and dems_land[0] != demname) 

253 

254 k = (step, demname, region, mask_ocean, mask_land) 

255 if k not in self._meshes: 

256 tile = topo.get(demname, region) 

257 if not tile: 

258 continue 

259 

260 self._meshes[k] = TopoMeshPipe( 

261 tile, 

262 cells_cache=self._cells, 

263 mask_ocean=mask_ocean, 

264 mask_land=mask_land, 

265 smooth=self._state.smooth, 

266 lut=self._lookuptables[self._state.cpt]) 

267 

268 wanted.add(k) 

269 

270 # prevent adding both, SRTM and ETOPO because 

271 # vtk produces artifacts when showing the two masked 

272 # meshes (like this, mask_land is never used): 

273 break 

274 

275 unwanted = set() 

276 for k in self._active_meshes: 

277 if k not in wanted or not visible: 

278 unwanted.add(k) 

279 

280 for k in unwanted: 

281 self._parent.remove_actor(self._active_meshes[k].actor) 

282 del self._active_meshes[k] 

283 

284 for k in wanted: 

285 if k not in self._active_meshes: 

286 m = self._meshes[k] 

287 self._active_meshes[k] = m 

288 self._parent.add_actor(m.actor) 

289 

290 self._active_meshes[k].set_exaggeration(self._state.exaggeration) 

291 self._active_meshes[k].set_opacity(self._state.opacity) 

292 self._active_meshes[k].set_smooth(self._state.smooth) 

293 self._active_meshes[k].set_shading(self._state.shading) 

294 self._active_meshes[k].set_lookuptable( 

295 self._lookuptables[self._state.cpt]) 

296 

297 self._parent.update_view() 

298 

299 def _get_controls(self): 

300 state = self._state 

301 if not self._controls: 

302 from ..state import state_bind_slider, state_bind_checkbox, \ 

303 state_bind_combobox 

304 

305 frame = qw.QFrame() 

306 layout = qw.QGridLayout() 

307 frame.setLayout(layout) 

308 

309 iy = 0 

310 

311 # exaggeration 

312 

313 layout.addWidget(qw.QLabel('Exaggeration'), iy, 0) 

314 

315 slider = qw.QSlider(qc.Qt.Horizontal) 

316 slider.setSizePolicy( 

317 qw.QSizePolicy( 

318 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

319 slider.setMinimum(0) 

320 slider.setMaximum(2000) 

321 layout.addWidget(slider, iy, 1) 

322 

323 state_bind_slider(self, state, 'exaggeration', slider, factor=0.01) 

324 

325 iy += 1 

326 

327 # opacity 

328 

329 layout.addWidget(qw.QLabel('Opacity'), iy, 0) 

330 

331 slider = qw.QSlider(qc.Qt.Horizontal) 

332 slider.setSizePolicy( 

333 qw.QSizePolicy( 

334 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

335 slider.setMinimum(0) 

336 slider.setMaximum(1000) 

337 layout.addWidget(slider, iy, 1) 

338 

339 state_bind_slider(self, state, 'opacity', slider, factor=0.001) 

340 

341 iy += 1 

342 

343 # high resolution 

344 

345 layout.addWidget(qw.QLabel('High-Res Factor'), iy, 0) 

346 

347 slider = qw.QSlider(qc.Qt.Horizontal) 

348 slider.setSizePolicy( 

349 qw.QSizePolicy( 

350 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

351 slider.setMinimum(500) 

352 slider.setMaximum(4000) 

353 layout.addWidget(slider, iy, 1) 

354 

355 state_bind_slider( 

356 self, state, 'resolution_max_factor', slider, factor=0.001) 

357 

358 iy += 1 

359 

360 # low resolution 

361 

362 layout.addWidget(qw.QLabel('Low-Res Factor'), iy, 0) 

363 

364 slider = qw.QSlider(qc.Qt.Horizontal) 

365 slider.setSizePolicy( 

366 qw.QSizePolicy( 

367 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

368 slider.setMinimum(500) 

369 slider.setMaximum(4000) 

370 layout.addWidget(slider, iy, 1) 

371 

372 state_bind_slider( 

373 self, state, 'resolution_min_factor', slider, factor=0.001) 

374 

375 iy += 1 

376 

377 # low resolution 

378 

379 layout.addWidget(qw.QLabel('Coverage Factor'), iy, 0) 

380 

381 slider = qw.QSlider(qc.Qt.Horizontal) 

382 slider.setSizePolicy( 

383 qw.QSizePolicy( 

384 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

385 slider.setMinimum(500) 

386 slider.setMaximum(4000) 

387 layout.addWidget(slider, iy, 1) 

388 

389 state_bind_slider( 

390 self, state, 'coverage_factor', slider, factor=0.001) 

391 

392 iy += 1 

393 

394 cb = common.string_choices_to_combobox(TopoCPTChoice) 

395 layout.addWidget(qw.QLabel('CPT'), iy, 0) 

396 layout.addWidget(cb, iy, 1) 

397 state_bind_combobox(self, state, 'cpt', cb) 

398 

399 iy += 1 

400 

401 cb = qw.QCheckBox('Smooth') 

402 layout.addWidget(cb, iy, 0) 

403 state_bind_checkbox(self, state, 'smooth', cb) 

404 

405 cb = common.string_choices_to_combobox(vstate.ShadingChoice) 

406 layout.addWidget(cb, iy, 1) 

407 state_bind_combobox(self, state, 'shading', cb) 

408 

409 iy += 1 

410 

411 layout.addWidget(qw.QFrame(), iy, 0, 1, 2) 

412 

413 self._controls = frame 

414 

415 return self._controls 

416 

417 

418__all__ = [ 

419 'TopoElement', 

420 'TopoState', 

421]