Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/sparrow/elements/topo.py: 95%
258 statements
« prev ^ index » next coverage.py v6.5.0, created at 2024-03-07 11:54 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2024-03-07 11:54 +0000
1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import math
7import numpy as num
9from pyrocko.guts import Bool, Float, StringChoice
10from pyrocko import cake, plot
11from pyrocko.plot import automap
12from pyrocko.dataset import topo
13from pyrocko.gui.qt_compat import qw, qc
15from pyrocko.gui.vtk_util import TrimeshPipe, faces_to_cells, \
16 cpt_to_vtk_lookuptable
18from .base import Element, ElementState
19from .. import state as vstate
20from pyrocko import geometry
22from .. import common
24guts_prefix = 'sparrow'
27def ticks(vmin, vmax, vstep):
28 vmin = num.floor(vmin / vstep) * vstep
29 vmax = num.ceil(vmax / vstep) * vstep
30 n = int(round((vmax - vmin) / vstep))
31 return vmin + num.arange(n+1) * vstep
34def nice_value_circle(step):
35 step = plot.nice_value(step)
36 if step > 30.:
37 return 30.
39 return step
42class TopoMeshPipe(TrimeshPipe):
44 def __init__(
45 self, tile, cells_cache=None,
46 mask_ocean=False, mask_land=False, **kwargs):
48 vertices, faces = geometry.topo_to_mesh(
49 tile.y(), tile.x(), tile.data,
50 cake.earthradius)
52 self._exaggeration = 1.0
53 self._tile = tile
54 self._raw_vertices = vertices
56 centers = geometry.face_centers(vertices, faces)
58 altitudes = (geometry.vnorm(centers) - 1.0) * cake.earthradius
59 if mask_ocean:
60 mask = num.all(tile.data.flatten()[faces] == 0, axis=1)
61 altitudes[mask] = None
63 if mask_land:
64 mask = num.all(tile.data.flatten()[faces] >= 0, axis=1)
65 altitudes[mask] = None
67 if cells_cache is not None:
68 if id(faces) not in cells_cache:
69 cells_cache[id(faces)] = faces_to_cells(faces)
71 cells = cells_cache[id(faces)]
72 else:
73 cells = faces_to_cells(faces)
75 TrimeshPipe.__init__(
76 self, self._raw_vertices, cells=cells, values=altitudes, **kwargs)
78 def set_exaggeration(self, exaggeration):
79 if self._exaggeration != exaggeration:
80 factors = \
81 (cake.earthradius + self._tile.data.flatten()*exaggeration) \
82 / (cake.earthradius + self._tile.data.flatten())
83 self.set_vertices(self._raw_vertices * factors[:, num.newaxis])
84 self._exaggeration = exaggeration
87class TopoCPTChoice(StringChoice):
88 choices = ['light', 'uniform']
91class TopoState(ElementState):
92 visible = Bool.T(default=True)
93 exaggeration = Float.T(default=1.0)
94 opacity = Float.T(default=1.0)
95 smooth = Bool.T(default=False)
96 cpt = TopoCPTChoice.T(default='light')
97 shading = vstate.ShadingChoice.T(default='phong')
98 resolution_max_factor = Float.T(default=1.0)
99 resolution_min_factor = Float.T(default=1.0)
100 coverage_factor = Float.T(default=1.0)
102 def create(self):
103 element = TopoElement()
104 return element
107class TopoElement(Element):
109 def __init__(self):
110 Element.__init__(self)
111 self._parent = None
112 self.mesh = None
113 self._controls = None
115 # region = (-180., 180, -90, 90)
116 # self._tile = topo.get('ETOPO1_D8', region)
118 self._visible = False
119 self._active_meshes = {}
120 self._meshes = {}
121 self._cells = {}
122 self._cpt_name = None
123 self._lookuptables = {}
125 def get_name(self):
126 return 'Topography'
128 def bind_state(self, state):
129 Element.bind_state(self, state)
130 self.talkie_connect(
131 state,
132 ['visible', 'exaggeration', 'opacity', 'smooth', 'shading', 'cpt',
133 'resolution_min_factor', 'resolution_max_factor',
134 'coverage_factor'],
135 self.update)
137 self._state = state
139 def set_parent(self, parent):
140 self._parent = parent
142 for var in ['distance', 'lat', 'lon']:
143 self.talkie_connect(
144 self._parent.state, var, self.update)
146 self._parent.add_panel(
147 self.get_title_label(),
148 self._get_controls(),
149 visible=True,
150 title_controls=[
151 self.get_title_control_remove(),
152 self.get_title_control_visible()])
154 self.update()
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)
162 self._active_meshes.clear()
163 self._meshes.clear()
164 self._cells.clear()
166 if self._controls:
167 self._parent.remove_panel(self._controls)
168 self._controls = None
170 self._parent.update_view()
171 self._parent = None
173 def select_dems(self, delta, region):
174 if not self._parent:
175 return [], []
177 _, size_y = self._parent.renwin.GetSize()
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)
184 result = [
185 topo.select_dem_names(
186 k, dmin, dmax, topo.positive_region(region), mode='highest')
188 for k in ['ocean', 'land']]
190 if not any(result) and delta > 20.:
191 return ['ETOPO1_D8'], ['ETOPO1_D8']
192 else:
193 return result
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'
201 elif cpt_name == 'uniform':
202 topo_cpt_wet = 'light_sea_uniform'
203 topo_cpt_dry = 'light_land_uniform'
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)
209 lut_combi = cpt_to_vtk_lookuptable(cpt_combi)
210 lut_combi.SetNanColor(0.0, 0.0, 0.0, 0.0)
212 self._lookuptables[cpt_name] = lut_combi
214 def update(self, *args):
216 pstate = self._parent.state
217 delta = pstate.distance * geometry.r2d * 0.5
218 visible = self._state.visible
220 self.update_cpt(self._state.cpt)
222 step = nice_value_circle(
223 max(1./8., min(2**round(math.log(delta) / math.log(2.)), 10.)))
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)
228 if lon_closed:
229 lon_max = 180.
231 region = lon_min, lon_max, lat_min, lat_max
233 dems_ocean, dems_land = self.select_dems(delta, region)
235 lat_majors = ticks(lat_min, lat_max-step, step)
236 lon_majors = ticks(lon_min, lon_max-step, step)
238 wanted = set()
239 if visible:
241 show_progress = [False]
242 abort = [False]
243 ntiles = lat_majors.size * lon_majors.size
244 itile = 0
246 def download_progress_update():
247 abort[0] = self._parent.progressbars.set_status(
248 'Downloading topography data',
249 itile/ntiles * 100., can_abort=False)
251 show_progress[0] = True
252 self._parent.update()
254 self._parent.download_progress_update.connect(
255 download_progress_update)
257 for ilat, lat in enumerate(lat_majors):
258 for ilon, lon in enumerate(lon_majors):
259 itile = ilon + ilat * lon_majors.size
260 lon = ((lon + 180.) % 360.) - 180.
262 region = topo.positive_region(
263 (lon, lon+step, lat, lat+step))
265 for demname in dems_land[:1] + dems_ocean[:1]:
266 mask_ocean = demname.startswith('SRTM') \
267 or demname.startswith('Iceland')
269 mask_land = demname.startswith('ETOPO') \
270 and (dems_land and dems_land[0] != demname)
272 k = (step, demname, region, mask_ocean, mask_land)
273 if k not in self._meshes:
274 tile = topo.get(demname, region)
275 if not tile:
276 continue
278 self._meshes[k] = TopoMeshPipe(
279 tile,
280 cells_cache=self._cells,
281 mask_ocean=mask_ocean,
282 mask_land=mask_land,
283 smooth=self._state.smooth,
284 lut=self._lookuptables[self._state.cpt])
286 wanted.add(k)
288 # prevent adding both, SRTM and ETOPO because
289 # vtk produces artifacts when showing the two masked
290 # meshes (like this, mask_land is never used):
291 break
293 if show_progress[0]:
294 download_progress_update()
296 if abort[0]:
297 break
299 if abort[0]:
300 break
302 itile = ntiles
303 if show_progress[0]:
304 download_progress_update()
306 self._parent.download_progress_update.disconnect(
307 download_progress_update)
309 if abort[0]:
310 self._state.visible = False
312 unwanted = set()
313 for k in self._active_meshes:
314 if k not in wanted or not visible:
315 unwanted.add(k)
317 for k in unwanted:
318 self._parent.remove_actor(self._active_meshes[k].actor)
319 del self._active_meshes[k]
321 for k in wanted:
322 if k not in self._active_meshes:
323 m = self._meshes[k]
324 self._active_meshes[k] = m
325 self._parent.add_actor(m.actor)
327 self._active_meshes[k].set_exaggeration(self._state.exaggeration)
328 self._active_meshes[k].set_opacity(self._state.opacity)
329 self._active_meshes[k].set_smooth(self._state.smooth)
330 self._active_meshes[k].set_shading(self._state.shading)
331 self._active_meshes[k].set_lookuptable(
332 self._lookuptables[self._state.cpt])
334 self._parent.update_view()
336 def _get_controls(self):
337 state = self._state
338 if not self._controls:
339 from ..state import state_bind_slider, state_bind_checkbox, \
340 state_bind_combobox
342 frame = qw.QFrame()
343 layout = qw.QGridLayout()
344 frame.setLayout(layout)
346 iy = 0
348 # exaggeration
350 layout.addWidget(qw.QLabel('Exaggeration'), iy, 0)
352 slider = qw.QSlider(qc.Qt.Horizontal)
353 slider.setSizePolicy(
354 qw.QSizePolicy(
355 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
356 slider.setMinimum(0)
357 slider.setMaximum(2000)
358 layout.addWidget(slider, iy, 1)
360 state_bind_slider(self, state, 'exaggeration', slider, factor=0.01)
362 iy += 1
364 # opacity
366 layout.addWidget(qw.QLabel('Opacity'), iy, 0)
368 slider = qw.QSlider(qc.Qt.Horizontal)
369 slider.setSizePolicy(
370 qw.QSizePolicy(
371 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
372 slider.setMinimum(0)
373 slider.setMaximum(1000)
374 layout.addWidget(slider, iy, 1)
376 state_bind_slider(self, state, 'opacity', slider, factor=0.001)
378 iy += 1
380 # high resolution
382 layout.addWidget(qw.QLabel('High-Res Factor'), iy, 0)
384 slider = qw.QSlider(qc.Qt.Horizontal)
385 slider.setSizePolicy(
386 qw.QSizePolicy(
387 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
388 slider.setMinimum(500)
389 slider.setMaximum(4000)
390 layout.addWidget(slider, iy, 1)
392 state_bind_slider(
393 self, state, 'resolution_max_factor', slider, factor=0.001)
395 iy += 1
397 # low resolution
399 layout.addWidget(qw.QLabel('Low-Res Factor'), iy, 0)
401 slider = qw.QSlider(qc.Qt.Horizontal)
402 slider.setSizePolicy(
403 qw.QSizePolicy(
404 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
405 slider.setMinimum(500)
406 slider.setMaximum(4000)
407 layout.addWidget(slider, iy, 1)
409 state_bind_slider(
410 self, state, 'resolution_min_factor', slider, factor=0.001)
412 iy += 1
414 # low resolution
416 layout.addWidget(qw.QLabel('Coverage Factor'), iy, 0)
418 slider = qw.QSlider(qc.Qt.Horizontal)
419 slider.setSizePolicy(
420 qw.QSizePolicy(
421 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
422 slider.setMinimum(500)
423 slider.setMaximum(4000)
424 layout.addWidget(slider, iy, 1)
426 state_bind_slider(
427 self, state, 'coverage_factor', slider, factor=0.001)
429 iy += 1
431 cb = common.string_choices_to_combobox(TopoCPTChoice)
432 layout.addWidget(qw.QLabel('CPT'), iy, 0)
433 layout.addWidget(cb, iy, 1)
434 state_bind_combobox(self, state, 'cpt', cb)
436 iy += 1
438 cb = qw.QCheckBox('Smooth')
439 layout.addWidget(cb, iy, 0)
440 state_bind_checkbox(self, state, 'smooth', cb)
442 cb = common.string_choices_to_combobox(vstate.ShadingChoice)
443 layout.addWidget(cb, iy, 1)
444 state_bind_combobox(self, state, 'shading', cb)
446 iy += 1
448 layout.addWidget(qw.QFrame(), iy, 0, 1, 2)
450 self._controls = frame
452 return self._controls
455__all__ = [
456 'TopoElement',
457 'TopoState',
458]