Source code for geosoft.gxpy.view

"""
Views, which can be 2D or 3D, contain groups of graphical elements that can be displayed to a user in a Geosoft
Map viewer or a Geosoft 3D viewer.  Geosoft maps can contain any number of 2D or 3D views.

Views contain one or more :class:`geosoft.gxpy.group.Group` instances.  2D views can contain 2D groups, while
3D views can contain both 2D and 3D groups.

:Classes:
    :`View`:        single 2D plane view
    :`View_3d`:     3D view in a `geosoft.3dv` file, or a 3D view on a 2D map.
    :`CrookedPath`: defines the path for a crooked section.

Both 2D and 3D views can be placed on a :class:`geosoft.gxpy.map.Map`, though 3D views
are stored in a `geosoft_3dv` file which can also be viewed separately from a map.

:Constants:
    :READ_ONLY: `geosoft.gxapi.MVIEW_READ`
    :WRITE_NEW: `geosoft.gxapi.MVIEW_WRITENEW`
    :WRITE_OLD: `geosoft.gxapi.MVIEW_WRITEOLD`
    :UNIT_VIEW: 0
    :UNIT_MAP: 2
    :UNIT_VIEW_UNWARPED: 3
    :GROUP_ALL: 0
    :GROUP_MARKED: 1
    :GROUP_VISIBLE: 2
    :GROUP_AGG: 3
    :GROUP_CSYMB: 4
    :GROUP_VOXD: 5
    :GROUP_VECTORVOX: 6
    :GROUP_SURFACE: 7
    :EXTENT_ALL: `geosoft.gxapi.MVIEW_EXTENT_ALL`
    :EXTENT_VISIBLE: `geosoft.gxapi.MVIEW_EXTENT_VISIBLE`
    :EXTENT_CLIPPED: `geosoft.gxapi.MVIEW_EXTENT_CLIP`

.. seealso:: :mod:`geosoft.gxpy.map`, :mod:`geosoft.gxpy.group`

   :mod:`geosoft.geosoft.gxapi.GXMVIEW`

.. note::

    Regression tests provide usage examples:
    `View tests <https://github.com/GeosoftInc/gxpy/blob/master/geosoft/gxpy/tests/test_view.py>`_

"""
import os
import numpy as np
from typing import NamedTuple

import geosoft
import geosoft.gxapi as gxapi
from . import gx as gx
from . import coordinate_system as gxcs
from . import utility as gxu
from . import map as gxmap
from . import metadata as gxmeta
from . import geometry as gxgeo
from . import spatialdata as gxspd
from . import vv as gxvv


__version__ = geosoft.__version__


def _t(s):
    return geosoft.gxpy.system.translate(s)


[docs]class ViewException(geosoft.GXRuntimeError): """ Exceptions from :mod:`geosoft.gxpy.view`. .. versionadded:: 9.2 """ pass
def _crooked_path_from_ipj(gxipj): if gxipj.get_orientation() != gxapi.IPJ_ORIENT_SECTION_CROOKED: raise ViewException(_t('This coordinate system does not define a crooked path')) dvv = gxvv.GXvv() xvv = gxvv.GXvv() yvv = gxvv.GXvv() log_z = gxapi.int_ref() gxipj.get_crooked_section_view_v_vs(dvv.gxvv, xvv.gxvv, yvv.gxvv, log_z) return dvv, xvv, yvv, log_z.value
[docs]class CrookedPath(gxgeo.Geometry): """ Description of a crooked (x, y) path that defines a crooked-section view, or a crooked-section grid. .. versionadded:: 9.4 """ def __str__(self): return 'CrookedPath "{}", {} points'.format(self.name, len(self))
[docs] def __init__(self, xy_path, log_z=False, **kw): super().__init__(**kw) if isinstance(xy_path, gxcs.Coordinate_system): self.coordinate_system = xy_path xy_path = xy_path.gxipj if isinstance(xy_path, gxapi.GXIPJ): d, x, y, self._log_z = _crooked_path_from_ipj(xy_path) self._xy = np.empty((x.length, 2)) self._xy[:, 0] = x.np self._xy[:, 1] = y.np self.coordinate_system = gxcs.Coordinate_system(xy_path) else: if not isinstance(xy_path, gxgeo.PPoint): xy_path = gxgeo.PPoint(xy_path, coordinate_system=self.coordinate_system) self._xy = xy_path.xy self.coordinate_system = xy_path.coordinate_system self._log_z = bool(log_z) # calculate a distance along the path dnp = np.zeros(len(self._xy), dtype=np.float64) dx = (self._xy[1:, 0] - self._xy[:-1, 0]) ** 2 dy = (self._xy[1:, 1] - self._xy[:-1, 1]) ** 2 dxy = np.sqrt((dx + dy)) dnp[1:] = dxy self._distances = dnp.cumsum()
def __len__(self): return len(self._xy) @property def xy(self): """Path trace as an array (npoints, 2).""" return self._xy @property def distances(self): """Distances along path points as an array (npoints) starting at 0.""" return self._distances @property def ppoint(self): """Path trace as a `geosoft.gxpy.geometry.PPoint` instance.""" return gxgeo.PPoint(self._xy, coordinate_system=self.coordinate_system)
[docs] def set_in_geosoft_ipj(self, coordinate_system): """ Set the crooked-path in the `geosoft.gxapi.GXIPJ` instance of the coordinate system. Geosoft stores crooked-path information in the GXIPJ, from which views are able to :param coordinate_system: .. versionadded:: 9.4 """ # make vv's to set the path dvv = gxvv.GXvv(self._distances) xvv = gxvv.GXvv(self._xy[:, 0]) yvv = gxvv.GXvv(self._xy[:, 1]) coordinate_system.gxipj.set_crooked_section_view(dvv.gxvv, xvv.gxvv, yvv.gxvv, self._log_z)
@property def extent(self): return self.ppoint.extent
[docs]class PlaneReliefSurfaceInfo(NamedTuple): """ Information about a relief surface assigned to a plane. The following properties are represented: surface_grid_name: grid file name refine: relief refinement between 1 (low) and 4 (high). Default is 3. base: base value in grid, will be at z=0. Default is 0. scale: scale to apply to grid after removing base, default is 1. min: minimum clip in unscaled grid values max: maximum clip in unscaled grid values .. versionadded:: 9.9 """ surface_grid_name: str refine: int base: float scale: float min: float max: float
[docs]def delete_files(v3d_file): """ Delete a v3d file with associated files. Just calls `geosoft.gxpy.map.delete_files`. The view must be closed. :param v3d_file: View_3d file name .. versionadded:: 9.3.1 """ gxmap.delete_files(v3d_file)
def _plane_err(plane, view): raise ViewException(_t('Plane "{}" does not exist in view "{}"'.format(plane, view))) VIEW_NAME_SIZE = 2080 READ_ONLY = gxapi.MVIEW_READ WRITE_NEW = gxapi.MVIEW_WRITENEW WRITE_OLD = gxapi.MVIEW_WRITEOLD UNIT_VIEW = 0 UNIT_MAP = 2 UNIT_VIEW_UNWARPED = 3 GROUP_ALL = 0 GROUP_MARKED = 1 GROUP_VISIBLE = 2 GROUP_AGG = 3 GROUP_CSYMB = 4 GROUP_VOXD = 5 GROUP_VECTORVOX = 6 GROUP_SURFACE = 7 _group_selector = (None, None, None, gxapi.MVIEW_IS_AGG, gxapi.MVIEW_IS_CSYMB, gxapi.MVIEW_IS_VOXD, gxapi.MVIEW_IS_VECTOR3D, None) EXTENT_ALL = gxapi.MVIEW_EXTENT_ALL EXTENT_VISIBLE = gxapi.MVIEW_EXTENT_VISIBLE EXTENT_CLIPPED = gxapi.MVIEW_EXTENT_CLIP
[docs]class View(gxgeo.Geometry): """ Geosoft view class. :Constructors: :`open`: open an existing view in a map :`new`: create a new view in a map .. versionadded:: 9.2 """ def __enter__(self): return self def __exit__(self, xtype, xvalue, xtraceback): self.__del__() def __del__(self): if hasattr(self, '_close'): self._close() def _close(self): if hasattr(self, '_open'): if self._open: self._gxview = None self._pen = None self._map = None # release map self._open = False def __repr__(self): return "{}({})".format(self.__class__, self.__dict__) def __str__(self): return self.name
[docs] def __init__(self, map, name="_unnamed_view", mode=WRITE_OLD, coordinate_system=None, map_location=(0, 0), area=(0, 0, 30, 20), scale=100, copy=None, gxmview=None, **kwargs): if not isinstance(map, geosoft.gxpy.map.Map): raise ViewException(_t('First argument must be a map.')) super().__init__(**kwargs) self._gx = gx.gx() self._map = map if gxmview is not None: name_ref = gxapi.str_ref() gxmview.get_name(name_ref) name = name_ref.value self._name = map.classview(name) self._gxview = gxmview else: self._name = map.classview(name) if mode == WRITE_OLD and not map.has_view(self._name): mode = WRITE_NEW self._gxview = gxapi.GXMVIEW.create(self._map.gxmap, self._name, mode) self._mode = mode self._lock = None self._open = True self._cs = None self._clip_mode = False if mode == WRITE_NEW: self.locate(coordinate_system, map_location, area, scale) if copy: with View(map, name=copy, mode=READ_ONLY) as v: v.gxview.mark_all_groups(1) v.gxview.copy_marked_groups(self.gxview) else: ipj = gxapi.GXIPJ.create() self.gxview.get_ipj(ipj) self._cs = gxcs.Coordinate_system(ipj) metres_per = self._cs.metres_per_unit self._uname = self._cs.units_name if metres_per <= 0.: raise ViewException(_t('Invalid units {}({})'.format(self._uname, metres_per))) self._metres_per_unit = 1.0 / metres_per
[docs] @classmethod def from_gxapi(cls, gxmap, gxmview): """ Instantiate View from gxapi instance. :param gxmap: a gxapi.CGXMAP :param gxmview: a gxapi.CGXMVIEW .. versionadded:: 9.9 """ return cls(geosoft.gxpy.map.Map.from_gxapi(gxmap), gxmview=gxmview)
[docs] @classmethod def new(cls, map=None, name="_unnamed_view", coordinate_system=None, map_location=(0, 0), area=(0, 0, 30, 20), scale=100, copy=None, crooked_path=None): """ Create a new view on a map. :parameters: :map: :class:`geosoft.gxpy.map.Map` instance, if not specified a new unique default map is created and deleted when this session finished. :name: view name, default is "_unnamed_view". :coordinate_system: coordinate system as a `geosoft.gxpy.coordinate_system.Coordinate_system` instance, or one of the Coordinate_system constructor types. :map_location: (x, y) view location on the map, in map cm, default (0, 0) :area: (min_x, min_y, max_x, max_y) area in view units, default (0, 0, 30, 20) :scale: Map scale if a coordinate system is defined. If the coordinate system is not defined this is view units per map metre. :copy: name of a view to copy into the new view. :crooked_path: provide a `CrookedPath` instance to create a section view along a wandering path. Should the coordinate system already contain a crooked path it will be replaced. .. versionadded:: 9.2 """ if map is None: map = gxmap.Map.new() view = cls(map, mode=WRITE_NEW, name=name, coordinate_system=coordinate_system, map_location=map_location, area=area, scale=scale, copy=copy) if crooked_path: if not isinstance(crooked_path, CrookedPath): crooked_path = CrookedPath(crooked_path, coordinate_system=view.coordinate_system) crooked_path.set_in_geosoft_ipj(view.coordinate_system) return view
[docs] @classmethod def open(cls, map, view_name, read_only=False): """ Open an en existing view on a map. :param map: :class:`geosoft.gxpy.map.Map` :param view_name: name of the view :param read_only: True to open read-only .. versionadded:: 9.2 """ if not map.has_view(view_name): raise ViewException(_t('Map does not have a view named \'{}\''.format(view_name))) if read_only: mode = READ_ONLY else: mode = WRITE_OLD view = cls(map, name=view_name, mode=mode) return view
@property def lock(self): """ True if the view is locked by a group. Only one group may hold a lock on a view at the same time. When drawing with groups you should use a `with gxgrp.Draw(...) as g:` which will ensure group locks are properly created and released. """ return self._lock @lock.setter def lock(self, group): if group: if self.lock: raise ViewException(_t('View is locked by group {}.').format(self.lock)) self._lock = group else: self._lock = None @property def is_crooked_path(self): """True if this grid follows a crooked path section.""" return self.coordinate_system.gxipj.get_orientation() == gxapi.IPJ_ORIENT_SECTION_CROOKED
[docs] def crooked_path(self): """ Return the `CrookedPath` instance for a crooked-path view. .. versionadded::9.4 """ if not self.is_crooked_path: raise ViewException(_t("This is not a crooked-path view.")) return CrookedPath(self.coordinate_system)
@property def clip(self): """ Current view clip mode for groups, applies to groups following in this stream. Can be set. .. versionadded:: 9.3.1 """ return self._clip_mode @clip.setter def clip(self, mode): self._clip_mode = bool(mode) self.gxview.group_clip_mode(int(mode)) @property def metadata(self): """ Return the view/map metadata as a dictionary. Can be set, in which case the dictionary items passed will be added to, or replace existing metadata. All views on a map share the metadata with the map. .. versionadded:: 9.2 """ return self.map.metadata @metadata.setter def metadata(self, meta): self.map.metadata = meta @property def coordinate_system(self): """ :class:`geosoft.gxpy.coordinate_system.Coordinate_system` instance of the view.""" if self._cs is None: self._cs = gxcs.Coordinate_system() return self._cs @property def gxview(self): """ The :class:`geosoft.gxapi.GXIPJ` instance handle.""" return self._gxview @coordinate_system.setter def coordinate_system(self, cs): if not isinstance(cs, gxcs.Coordinate_system): cs = gxcs.Coordinate_system(cs) self._cs = gxcs.Coordinate_system(cs) metres_per = self._cs.metres_per_unit self._uname = self._cs.units_name if metres_per <= 0.: raise ViewException(_t('Invalid units {}({})'.format(self._uname, metres_per))) self._metres_per_unit = 1.0 / metres_per self.gxview.set_ipj(self._cs.gxipj) @property def map_scale(self): """ Map scale for this view. Can be set, in which case the entire view will move on the map. """ return self.gxview.get_map_scale() @map_scale.setter def map_scale(self, s): if s > 0.0: self.gxview.re_scale(s)
[docs] def close(self): """ Close a view. Use to close a view when working outside of a `with ... as:` construct. .. versionadded:: 9.2 """ self._close()
[docs] def add_child_files(self, file_list): """ Add files to the list of child files for this view. :param file_list: file, or a list of files to add .. versionaddded 9.3.1 """ meta = self.metadata node = 'geosoft/dataset/map/views/' + self.name + '/child_files' child_files = gxmeta.get_node_from_meta_dict(node, meta) if child_files is None: child_files = [] if isinstance(file_list, str): child_files.append(file_list) else: for f in file_list: if f not in child_files: child_files.append(f) gxmeta.set_node_in_meta_dict(node, meta, child_files) self.metadata = meta
[docs] def locate(self, coordinate_system=None, map_location=None, area=None, scale=None): """ Locate and scale the view on the map. :parameters: :coordinate_system: coordinate system as a class:`gxpy.coordinate_system.Coordinate_system` instance, or one of the Coordinate_system constructor types. :map_location: New (x, y) view location on the map, in map cm. :area: New (min_x, min_y, max_x, max_y) area in view units :scale: New scale in view units per map metre, either as a single value or (x_scale, y_scale), defaults to the current x scale. .. versionadded:: 9.2 """ if self._mode == READ_ONLY: raise ViewException(_t('Cannot modify a READ_ONLY view.')) # coordinate system if coordinate_system: self.coordinate_system = coordinate_system upm = self.units_per_metre if area is None: area = self.extent_clip # area and scale if scale is None: if self.scale is None: raise ViewException(_t('A scale is required.')) scale = self.scale if hasattr(scale, "__iter__"): x_scale, y_scale = scale else: x_scale = y_scale = scale a_minx, a_miny, a_maxx, a_maxy = area if map_location is None: map_location = (0., 0.) mm_minx = map_location[0] * 10.0 mm_miny = map_location[1] * 10.0 mm_maxx = mm_minx + (a_maxx - a_minx) * 1000.0 / upm / x_scale mm_maxy = mm_miny + (a_maxy - a_miny) * 1000.0 / upm / y_scale self.gxview.fit_window(mm_minx, mm_miny, mm_maxx, mm_maxy, a_minx, a_miny, a_maxx, a_maxy) self.gxview.set_window(a_minx, a_miny, a_maxx, a_maxy, UNIT_VIEW)
@property def map(self): """ :class:`geosoft.gxpy.map.Map` instance that contains this view.""" return self._map @property def name(self): """ Name of the view""" return self._name @property def is_3d(self): """True if this is a 3D view""" return bool(self.gxview.is_view_3d()) @property def units_per_metre(self): """view units per view metres (eg. a view in 'ft' will be 3.28084)""" return 1.0 / self.coordinate_system.metres_per_unit @property def units_per_map_cm(self): """view units per map cm. (eg. a view in ft, with a scale of 1:12000 returns 393.7 ft/cm)""" return self.gxview.scale_mm() * 10.0 @property def units_name(self): """name of the view distance units""" return self.coordinate_system.units_name @property def guid(self): """ The view GUID. .. versionadded:: 9.3 """ sr = gxapi.str_ref() self.gxview.get_guid(sr) return sr.value
[docs] def mdf(self, base_view=None): """ Returns the Map Description File specification for this view as a data view. :param base_view: name of the base view on the map from which to calculate margins. If not specified only the left and bottom margin is calculated based on the view clip minimum location and the right and top margins will be 0. :returns: ((x_size, y_size, margin_bottom, margin_right, margin_top, margin_left), (scale, units_per_metre, x_origin, y_origin)) .. versionadded: 9.2 """ view_mnx, view_mny, view_mxx, view_mxy = self.extent_clip map_mnx, map_mny = self.view_to_map_cm(view_mnx, view_mny) map_mxx, map_mxy = self.view_to_map_cm(view_mxx, view_mxy) if base_view: if not isinstance(base_view, View): base_view = View(self.map, base_view) _, _, mapx, mapy = base_view.extent_clip mapx, mapy = base_view.view_to_map_cm(mapx, mapy) else: mapx, mapy = map_mxx, map_mxy m1 = (mapx, mapy, map_mny, mapx - map_mxx, mapy - map_mxy, map_mnx) m2 = (self.scale, self.units_per_metre, view_mnx, view_mny) return m1, m2
def _groups(self, gtype=GROUP_ALL): def gdict(what): self.gxview.list_groups(gxlst, what) return gxu.dict_from_lst(gxlst) gxlst = gxapi.GXLST.create(VIEW_NAME_SIZE) if gtype == GROUP_ALL: return list(gdict(gxapi.MVIEW_GROUP_LIST_ALL)) elif gtype == GROUP_MARKED: return list(gdict(gxapi.MVIEW_GROUP_LIST_MARKED)) elif gtype == GROUP_VISIBLE: return list(gdict(gxapi.MVIEW_GROUP_LIST_VISIBLE)) # filter by type wanted gd = gdict(gxapi.MVIEW_GROUP_LIST_ALL) groups = [] if gtype == GROUP_SURFACE: for g in gd: if g[:5] == 'SURF_': groups.append(g) else: isg = _group_selector[gtype] for g in gd: if self.gxview.is_group(g, isg): groups.append(g) return groups @property def group_list(self): """list of group names in this view""" return self._groups() @property def group_list_marked(self): """list of marked group names in this view""" return self._groups(GROUP_MARKED) @property def group_list_visible(self): """list of visible group names in this view""" return self._groups(GROUP_VISIBLE) @property def group_list_agg(self): """list of aggregate group names in this view""" return self._groups(GROUP_AGG) @property def group_list_csymb(self): """list of csymb group names in this view""" return self._groups(GROUP_CSYMB) @property def group_list_voxel(self): """list of voxel group names in this view""" return self._groups(GROUP_VOXD) @property def group_list_vectorvoxel(self): """list of vectorvoxel group names in this view""" return self._groups(GROUP_VECTORVOX) @property def group_list_surface(self): """list of surface group names in this view""" return self._groups(GROUP_SURFACE)
[docs] def has_group(self, group): """ Returns True if the map contains this group by name.""" return bool(self.gxview.exist_group(group))
def _extent(self, what): xmin = gxapi.float_ref() ymin = gxapi.float_ref() xmax = gxapi.float_ref() ymax = gxapi.float_ref() self.gxview.extent(what, UNIT_VIEW, xmin, ymin, xmax, ymax) return xmin.value, ymin.value, xmax.value, ymax.value @property def extent(self): """ View clip extent as a `geosoft.gxpy.geometry.Point2`. .. versionadded:: 9.3.1 """ cs = self.coordinate_system ex2d = self.extent_clip if self.is_crooked_path: min_x, min_y, max_x, max_y = self.crooked_path().extent_xy min_z = cs.xyz_from_oriented((ex2d[0], ex2d[1], 0.0))[2] max_z = cs.xyz_from_oriented((ex2d[0], ex2d[3], 0.0))[2] else: xyz0 = cs.xyz_from_oriented((ex2d[0], ex2d[1], 0.0)) xyz1 = cs.xyz_from_oriented((ex2d[2], ex2d[1], 0.0)) xyz2 = cs.xyz_from_oriented((ex2d[2], ex2d[3], 0.0)) xyz3 = cs.xyz_from_oriented((ex2d[0], ex2d[3], 0.0)) min_x = min(xyz0[0], xyz1[0], xyz2[0], xyz3[0]) min_y = min(xyz0[1], xyz1[1], xyz2[1], xyz3[1]) min_z = min(xyz0[2], xyz1[2], xyz2[2], xyz3[2]) max_x = max(xyz0[0], xyz1[0], xyz2[0], xyz3[0]) max_y = max(xyz0[1], xyz1[1], xyz2[1], xyz3[1]) max_z = max(xyz0[2], xyz1[2], xyz2[2], xyz3[2]) return gxgeo.Point2(((min_x, min_y, min_z), (max_x, max_y, max_z)), self.coordinate_system) @property def extent_clip(self): """clip extent of the view as (x_min, y_min, x_max, y_max)""" return self._extent(gxapi.MVIEW_EXTENT_CLIP) @property def extent_all(self): """extent of all groups in the view as (x_min, y_min, x_max, y_max)""" return self._extent(gxapi.MVIEW_EXTENT_ALL) @property def extent_visible(self): """extent of visible groups in the view as (x_min, y_min, x_max, y_max)""" return self._extent(gxapi.MVIEW_EXTENT_VISIBLE)
[docs] def extent_map_cm(self, extent=None): """ Return an extent in map cm. :param extent: tuple returned from one of the extent properties. Default is :attr:`extent_clip`. .. versionadded:: 9.2 """ if extent is None: extent = self.extent_clip xmin, ymin = self.view_to_map_cm(extent[0], extent[1]) xmax, ymax = self.view_to_map_cm(extent[2], extent[3]) return xmin, ymin, xmax, ymax
@property def scale(self): """map scale for the view""" return 1000.0 * self.gxview.scale_mm() * self.coordinate_system.metres_per_unit @property def aspect(self): """view aspect ratio, usually 1.""" return self.gxview.scale_ymm() / self.gxview.scale_mm()
[docs] def extent_group(self, group, unit=UNIT_VIEW): """ Extent of a group :param group: group name :param unit: units: :: UNIT_VIEW UNIT_MAP :returns: extent as (x_min, y_min, x_max, y_max) .. versionadded: 9.2 """ xmin = gxapi.float_ref() ymin = gxapi.float_ref() xmax = gxapi.float_ref() ymax = gxapi.float_ref() self.gxview.get_group_extent(group, xmin, ymin, xmax, ymax, unit) if unit == UNIT_MAP: xmin.value *= 0.1 xmax.value *= 0.1 ymin.value *= 0.1 ymax.value *= 0.1 return xmin.value, ymin.value, xmax.value, ymax.value
[docs] def delete_group(self, group_name): """ Delete a group from a map. Nothing happens if the view does not contain this group. :param group_name: Name of the group to delete. .. versionadded:: 9.2 """ self.gxview.delete_group(group_name)
[docs] def map_cm_to_view(self, x, y=None): """ Returns the location of this point on the map (in cm) to the view location in view units. :param x: x, or a tuple (x,y), in map cm :param y: y if x is not a tuple .. versionadded:: 9.2 """ if y is None: y = x[1] x = x[0] xr = gxapi.float_ref() xr.value = x * 10.0 yr = gxapi.float_ref() yr.value = y * 10.0 self.gxview.plot_to_view(xr, yr) return xr.value, yr.value
[docs] def view_to_map_cm(self, x, y=None): """ Returns the location of this point on the map in the view. :param x: x, or a tuple (x,y), in view units :param y: y if x is not a tuple .. versionadded:: 9.2 """ if y is None: y = x[1] x = x[0] xr = gxapi.float_ref() xr.value = x yr = gxapi.float_ref() yr.value = y self.gxview.view_to_plot(xr, yr) return xr.value / 10.0, yr.value / 10.0
[docs] def get_class_name(self, view_class): """ Get the name associated with a view class. :param view_class: desired class in this view Common view class names are:: 'Plane' the name of the default 2D drawing plane 'Section' a section view Other class names may be defined, though they are not used by Geosoft. :returns: name associated with the class, '' if not defined. .. versionadded:: 9.2 """ sr = gxapi.str_ref() self.gxview.get_class_name(view_class, sr) return sr.value.lower()
[docs] def set_class_name(self, view_class, name): """ Set the name associated with a class. :param view_class: class name in this view :param name: name of the view associated with this class. Common view class names are:: 'Plane' the name of the default 2D drawing plane 'Section' a section view .. versionadded:: 9.2 """ self.gxview.set_class_name(view_class, name)
[docs]class View_3d(View): """ Geosoft 3D views, which contain 3D drawing groups. Geosoft 3D views are stored in a file with extension `.geosoft_3dv`. A 3d view is required to draw 3D elements using :class:`geosoft.gxpy.group.Draw_3d`, which must be created from a :class:`geosoft.gxpy.view.View_3d` instance. 3D views also contain 2D drawing planes on which :class:`geosoft.gxpy.group.Draw` groups are placed. A default horizontal plane at elevation 0, named 'plane_0' is created when a new 3d view is created. Planes are horizontal and flat by default, but can be provided a grid that defines the plane surface relief, which is intended for creating things like terrain surfaces on which 2d graphics are rendered. Planes can also be oriented within the 3D space to create sections, or for other more esoteric purposes. :Constructors: ============ ============================= :meth:`open` open an existing geosoft_3dv :meth:`new` create a new geosoft_3dv ============ ============================= .. versionadded:: 9.2 """
[docs] def __init__(self, file_name, mode, _internal=False, map=None, gxmview=None, **kwargs): if not _internal: raise ViewException(_t("Must be called by a class constructor 'open' or 'new' or 'from_gxapi'")) if map and gxmview: super().__init__(map, gxmview=gxmview, **kwargs) else: file_name = geosoft.gxpy.map.map_file_name(file_name, 'geosoft_3dv') map = geosoft.gxpy.map.Map(file_name=file_name, mode=mode, _internal=True) super().__init__(map, '3D', **kwargs) self._extent3d = None
def _extent_union(self, extent): """Expand the extent""" if self._extent is None: self._extent = gxgeo.Point2(extent, self.coordinate_system) else: self._extent = self._extent.union(extent)
[docs] @classmethod def from_gxapi(cls, gxmap, gxmview): """ Instantiate View_3d from gxapi instance. :param gxmap: a gxapi.CGXMAP :param gxmview: a gxapi.CGXMVIEW .. versionadded:: 9.9 """ return cls(file_name=None, mode=WRITE_OLD, _internal=True, map=geosoft.gxpy.map.Map.from_gxapi(gxmap), gxmview=gxmview)
[docs] @classmethod def new(cls, file_name=None, area_2d=None, overwrite=False, **kwargs): """ Create a new 3D view. :param file_name: name for the new 3D view file (.geosoft_3dv added). If not specified a unique temporary file is created. :param area_2d: 2D drawing extent for the default 2D drawing plane :param overwrite: True to overwrite an existing 3DV .. versionadded:: 9.2 """ if file_name is None: file_name = gx.gx().temp_file('.geosoft_3dv') else: file_name = geosoft.gxpy.map.map_file_name(file_name, 'geosoft_3dv') if not overwrite: if os.path.isfile(file_name): raise ViewException(_t('Cannot overwrite existing file: {}').format(file_name)) g_3dv = cls(file_name, geosoft.gxpy.map.WRITE_NEW, area=area_2d, _internal=True, **kwargs) map_minx, map_miny, map_maxx, map_maxy = g_3dv.extent_map_cm(g_3dv.extent_clip) view_minx, view_miny, view_maxx, view_maxy = g_3dv.extent_clip # make this a 3D view h3dn = gxapi.GX3DN.create() g_3dv.gxview.set_3dn(h3dn) g_3dv.gxview.fit_map_window_3d(map_minx, map_miny, map_maxx, map_maxy, view_minx, view_miny, view_maxx, view_maxy) return g_3dv
[docs] @classmethod def open(cls, file_name, **kw): """ Open an existing geosoft_3dv file. :param file_name: name of the geosoft_3dv file .. versionadded:: 9.2 """ file_name = geosoft.gxpy.map.map_file_name(file_name, 'geosoft_3dv') if not os.path.isfile(file_name): raise ViewException(_t('geosoft_3dv file not found: {}').format(file_name)) g_3dv = cls(file_name, geosoft.gxpy.map.WRITE_OLD, _internal=True) # read extents from the metadata try: g_3dv.add_extent(gxspd.extent_from_metadata(g_3dv.metadata)) except KeyError: pass return g_3dv
def __exit__(self, xtype, xvalue, xtraceback): self.__del__() def __del__(self): if hasattr(self, 'close'): self.close()
[docs] def close(self): """close the view, releases resources.""" if hasattr(self, 'map'): if self.map: self.map.close() if hasattr(self, '_close'): self._close()
[docs] def add_extent(self, extent): """ Expand current extent to include this extent. :param extent: extent as a `geosoft.gxpy.geometry.Geometry` or Point2 constructor TODO: review once issue #75 is resolved. .. versionadded:: 9.3.1 """ self._extent3d = gxgeo.extent_union(self._extent3d, extent)
@property def extent(self): """ Extent of 3D objects in this view. :return: `geosoft.gxpy.geometry.Point2` instance TODO: review once issue #75 is resolved. .. versionadded:: 9.3.1 """ return self._extent3d @property def file_name(self): """ the `geosoft_3dv` file name""" return self.map.file_name @property def name(self): """the view name""" return self.map.name @property def current_3d_drawing_plane(self): """Current drawing plane name in a 3D view, `None` if not defined. Can be set to a plane number or name.""" if len(self.plane_list): s = gxapi.str_ref() self.gxview.get_def_plane(s) return s.value else: return None @current_3d_drawing_plane.setter def current_3d_drawing_plane(self, plane): if plane: if isinstance(plane, int): plane = self.plane_name(plane) if plane not in self.plane_list: self.new_drawing_plane(plane) self.gxview.set_def_plane(plane) @property def current_3d_drawing_plane_number(self): """The current drawing plane number, can be set.""" return self.plane_number(self.current_3d_drawing_plane) @current_3d_drawing_plane_number.setter def current_3d_drawing_plane_number(self, plane): self.current_3d_drawing_plane = plane @property def plane_list(self): """list of drawing planes in the view""" gxlst = gxapi.GXLST.create(VIEW_NAME_SIZE) self.gxview.list_planes(gxlst) return list(gxu.dict_from_lst(gxlst))
[docs] def plane_name(self, plane): """Return the name of a numbered plane""" if isinstance(plane, str): if self.gxview.find_plane(plane) == -1: _plane_err(plane, self.name) return plane gxlst = gxapi.GXLST.create(VIEW_NAME_SIZE) self.gxview.list_planes(gxlst) item = gxlst.find_item(gxapi.LST_ITEM_VALUE, str(plane)) if item == -1: _plane_err(plane, self.name) sr = gxapi.str_ref() gxlst.gt_item(gxapi.LST_ITEM_NAME, item, sr) return sr.value
[docs] def plane_number(self, plane): """Return the plane number of a plane, or None if plane does not exist.""" if plane: if isinstance(plane, int): self.plane_name(plane) return plane plane_number = self.gxview.find_plane(plane) if plane_number == -1: _plane_err(plane, self.name) else: return plane_number else: return None
[docs] def delete_plane(self, plane): """ Delete a plane, and all content :param plane: plane number or plane name .. versionadded:: 9.3.1 """ if isinstance(plane, str): plane = self.plane_number(plane) try: self.gxview.delete_plane(plane, True) except gxapi.GXError: pass
[docs] def has_plane(self, plane): """ True if the view contains plane :param plane: name of the plane :returns: True if the plane exists in the view .. versionadded:: 9.2 """ try: self.plane_number(plane) return True except ViewException: return False
[docs] def groups_on_plane_list(self, plane=0): """ List of groups on a plane. :param plane: name of the plane or plane number :returns: list of groups on the plane .. versionadded:: 9.2 """ gxlst = gxapi.GXLST.create(VIEW_NAME_SIZE) if isinstance(plane, str): plane = self.plane_number(plane) self.gxview.list_plane_groups(plane, gxlst) return list(gxu.dict_from_lst(gxlst))
[docs] def new_drawing_plane(self, name, rotation=(0., 0., 0.), offset=(0., 0., 0.), scale=(1., 1., 1.)): """ Create a new drawing plane in a 3d view. :param name: name of the plane, overwritten if it exists :param rotation: plane rotation as (rx, ry, rz), default (0, 0, 0) :param offset: (x, y, z) offset of the plane, default (0, 0, 0) :param scale: (xs, ys, zs) axis scaling, default (1, 1, 1) .. versionadded::9.2 """ if self.has_plane(name): raise ViewException(_t('3D drawing plane "{}" exists.'.format(name))) self.gxview.create_plane(str(name)) self.gxview.set_plane_equation(self.plane_number(name), rotation[0], rotation[1], rotation[2], offset[0], offset[1], offset[2], scale[0], scale[1], scale[2])
[docs] def get_plane_relief_surface_info(self, plane): """ Get relief surface parameters for a plane. :param plane: plane number or plane name :returns: relief surface properties :rtype: :class:`geosoft.gxpy.view.PlaneReliefSurfaceInfo` .. versionadded::9.2 """ if isinstance(plane, str): plane = self.plane_number(plane) surface_grid_name = gxapi.str_ref() sample = gxapi.int_ref() base = gxapi.float_ref() scale = gxapi.float_ref() min_ref = gxapi.float_ref() max_ref = gxapi.float_ref() self.gxview.get_plane_surface(plane, surface_grid_name) self.gxview.get_plane_surf_info(plane, sample, base, scale, min_ref, max_ref) refine = 1 + int(sample.value / 16) min_val = None if min_ref.value == gxapi.rDUMMY else min_ref.value max_val = None if max_ref.value == gxapi.rDUMMY else max_ref.value return PlaneReliefSurfaceInfo(surface_grid_name.value, refine, base.value, scale.value, min_val, max_val)
[docs] def set_plane_relief_surface(self, surface_grid_name, refine=3, base=0, scale=1, min=None, max=None): """ Establish a relief surface for the current plane based on a grid. :param surface_grid_name: grid file name :param refine: relief refinement between 1 (low) and 4 (high). Default is 3. :param base: base value in grid, will be at z=0. Default is 0. :param scale: scale to apply to grid after removing base, default is 1. :param min: minimum clip in unscaled grid values :param max: maximum clip in unscaled grid values .. versionadded:: 9.3 """ if not self.current_3d_drawing_plane: name = os.path.basename(surface_grid_name).split('.')[0] self.current_3d_drawing_plane = name self.gxview.set_plane_surface(self.current_3d_drawing_plane_number, surface_grid_name) if min is None: min = gxapi.rDUMMY if max is None: max = gxapi.rDUMMY refine = int(refine) if refine <= 1: refine = 1 elif refine >= 4: refine = 48 else: refine = (refine - 1) * 16 self.gxview.set_plane_surf_info(self.current_3d_drawing_plane_number, refine, base, scale, min, max)