Source code for geosoft.gxpy.group

"""
A Geosoft View (:class:`geosoft.gxpy.view.View` or :class:`geosoft.gxpy.view.View_3d`) contains graphical elements as
`Group` instances. Groups are named and are available to a user in a Geosoft viewer, which allows groups to
be turned on or off, modify the transparency, or be deleted.

2D views can only accept 2D groups, while a 3D view can accept both 2D and 3D groups. When a 2D group is placed
in a 3D view, the group is placed on a the active plane inside the 3D view

:Classes:
    :`Group`:               base class for named rendering groups in 2D and 3D views.
    :`Draw`:                2D drawing group, handles 2D drawing to a view or plane in a 3D view
    :`Draw_3d`:             3D grawing group for 3D objects placed in a 3d view
    :`Color_symbols_group`: group for 2D symbols rendered based on data values
    :`Aggregate_group`:     group that contains a :class:`geosoft.gxpy.agg.Aggregate_image` instance
    :`Color`:               colour definition
    :`Color_map`:           maps values to colors
    :`Pen`:                 pen definition, includes line colour, thickness and pattern, and fill.
    :`Text_def`:            defined text characteristics
    :`VoxDisplayGroup`:     a 'geosoft.gxpy.vox.VoxDisplay` in a `geosoft.gxpy.view.View_3d`

:Constants:
    :GROUP_NAME_SIZE: `geosoft.gxpy.view.VIEW_NAME_SIZE`
    :NEW: `geosoft.gxapi.MVIEW_GROUP_NEW`
    :APPEND: `geosoft.gxapi.MVIEW_GROUP_APPEND`
    :READ_ONLY: max(NEW, APPEND) + 1
    :REPLACE: READ_ONLY + 1

    :SMOOTH_NONE: `geosoft.gxapi.MVIEW_SMOOTH_NEAREST`
    :SMOOTH_CUBIC: `geosoft.gxapi.MVIEW_SMOOTH_CUBIC`
    :SMOOTH_AKIMA: `geosoft.gxapi.MVIEW_SMOOTH_AKIMA`

    :TILE_RECTANGULAR: `geosoft.gxapi.MVIEW_TILE_RECTANGULAR`
    :TILE_DIAGONAL: `geosoft.gxapi.MVIEW_TILE_DIAGONAL`
    :TILE_TRIANGULAR: `geosoft.gxapi.MVIEW_TILE_TRIANGULAR`
    :TILE_RANDOM: `geosoft.gxapi.MVIEW_TILE_RANDOM`

    :UNIT_VIEW: 0
    :UNIT_MAP: 2
    :UNIT_VIEW_UNWARPED: 3

    :GRATICULE_DOT: 0
    :GRATICULE_LINE: 1
    :GRATICULE_CROSS: 2

    :LINE_STYLE_SOLID: 1
    :LINE_STYLE_LONG: 2
    :LINE_STYLE_DOTTED: 3
    :LINE_STYLE_SHORT: 4
    :LINE_STYLE_LONG_SHORT_LONG: 5
    :LINE_STYLE_LONG_DOT_LONG: 6

    :SYMBOL_NONE: 0
    :SYMBOL_DOT: 1
    :SYMBOL_PLUS: 2
    :SYMBOL_X: 3
    :SYMBOL_BOX: 4
    :SYMBOL_TRIANGLE: 5
    :SYMBOL_INVERTED_TRIANGLE: 6
    :SYMBOL_HEXAGON: 7
    :SYMBOL_SMALL_BOX: 8
    :SYMBOL_SMALL_DIAMOND: 9
    :SYMBOL_CIRCLE: 20

    :SYMBOL_3D_SPHERE: 0
    :SYMBOL_3D_CUBE: 1
    :SYMBOL_3D_CYLINDER: 2
    :SYMBOL_3D_CONE: 3

    :FONT_WEIGHT_ULTRALIGHT: 1
    :FONT_WEIGHT_LIGHT: 2
    :FONT_WEIGHT_MEDIUM: 3
    :FONT_WEIGHT_BOLD: 4
    :FONT_WEIGHT_XBOLD: 5
    :FONT_WEIGHT_XXBOLD: 6

    :CMODEL_RGB: 0
    :CMODEL_CMY: 1
    :CMODEL_HSV: 2

    :C_BLACK: 67108863
    :C_RED: 33554687
    :C_GREEN: 33619712
    :C_BLUE: 50266112
    :C_CYAN: 50331903
    :C_MAGENTA: 50396928
    :C_YELLOW: 67043328
    :C_GREY: 41975936
    :C_LT_RED: 54542336
    :C_LT_GREEN: 54526016
    :C_LT_BLUE: 50348096
    :C_LT_CYAN: 50331712
    :C_LT_MAGENTA: 50348032
    :C_LT_YELLOW: 54525952
    :C_LT_GREY: 54542400
    :C_GREY10: 51910680
    :C_GREY25: 54542400
    :C_GREY50: 41975936
    :C_WHITE: 50331648
    :C_TRANSPARENT: 0

    :REF_BOTTOM_LEFT: 0
    :REF_BOTTOM_CENTER: 1
    :REF_BOTTOM_RIGHT: 2
    :REF_CENTER_LEFT: 3
    :REF_CENTER: 4
    :REF_CENTER_RIGHT: 5
    :REF_TOP_LEFT: 6
    :REF_TOP_CENTER: 7
    :REF_TOP_RIGHT: 8

    :GROUP_ALL: 0
    :GROUP_MARKED: 1
    :GROUP_VISIBLE: 2
    :GROUP_AGG: 3
    :GROUP_CSYMB: 4
    :GROUP_VOXD: 5

    :LOCATE_FIT: `geosoft.gxapi.MVIEW_RELOCATE_FIT`
    :LOCATE_FIT_KEEP_ASPECT: `geosoft.gxapi.MVIEW_RELOCATE_ASPECT`
    :LOCATE_CENTER: `geosoft.gxapi.MVIEW_RELOCATE_ASPECT_CENTER`

    :COLOR_BAR_RIGHT: 0
    :COLOR_BAR_LEFT: 1
    :COLOR_BAR_BOTTOM: 2
    :COLOR_BAR_TOP: 3

    :COLOR_BAR_ANNOTATE_RIGHT: 1
    :COLOR_BAR_ANNOTATE_LEFT: -1
    :COLOR_BAR_ANNOTATE_TOP: 1
    :COLOR_BAR_ANNOTATE_BOTTOM: -1

    :CYLINDER_OPEN: 0
    :CYLINDER_CLOSE_START: 1
    :CYLINDER_CLOSE_END: 2
    :CYLINDER_CLOSE_ALL: 3

    :POINT_STYLE_DOT: 0
    :POINT_STYLE_SPHERE: 1

    :LINE3D_STYLE_LINE: 0
    :LINE3D_STYLE_TUBE: 1
    :LINE3D_STYLE_TUBE_JOINED: 2

    :SURFACE_FLAT: `geosoft.gxapi.MVIEW_DRAWOBJ3D_MODE_FLAT`
    :SURFACE_SMOOTH: `geosoft.gxapi.MVIEW_DRAWOBJ3D_MODE_SMOOTH`

.. note::

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

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

   :class:`geosoft.gxapi.GXMVIEW`, :class:`geosoft.gxapi.GXMVU`

"""
from functools import wraps
import threading
import os
import numpy as np

import geosoft
import geosoft.gxapi as gxapi
from . import gx
from . import vv as gxvv
from . import geometry as gxgm
from . import coordinate_system as gxcs
from . import utility as gxu
from . import view as gxv
from . import agg as gxagg
from . import metadata as gxmeta
from . import vox_display as gxvoxd
from . import spatialdata as gxspd


__version__ = geosoft.__version__


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


MAX_TRANSPARENT = 4


[docs]class GroupException(geosoft.GXRuntimeError): """ Exceptions from :mod:`geosoft.gxpy.group`. .. versionadded:: 9.2 """ pass
GROUP_NAME_SIZE = gxv.VIEW_NAME_SIZE NEW = gxapi.MVIEW_GROUP_NEW APPEND = gxapi.MVIEW_GROUP_APPEND READ_ONLY = max(NEW, APPEND) + 1 REPLACE = READ_ONLY + 1 SMOOTH_NONE = gxapi.MVIEW_SMOOTH_NEAREST SMOOTH_CUBIC = gxapi.MVIEW_SMOOTH_CUBIC SMOOTH_AKIMA = gxapi.MVIEW_SMOOTH_AKIMA TILE_RECTANGULAR = gxapi.MVIEW_TILE_RECTANGULAR TILE_DIAGONAL = gxapi.MVIEW_TILE_DIAGONAL TILE_TRIANGULAR = gxapi.MVIEW_TILE_TRIANGULAR TILE_RANDOM = gxapi.MVIEW_TILE_RANDOM UNIT_VIEW = 0 UNIT_MAP = 2 UNIT_VIEW_UNWARPED = 3 GRATICULE_DOT = 0 GRATICULE_LINE = 1 GRATICULE_CROSS = 2 LINE_STYLE_SOLID = 1 LINE_STYLE_LONG = 2 LINE_STYLE_DOTTED = 3 LINE_STYLE_SHORT = 4 LINE_STYLE_LONG_SHORT_LONG = 5 LINE_STYLE_LONG_DOT_LONG = 6 SYMBOL_NONE = 0 SYMBOL_DOT = 1 SYMBOL_PLUS = 2 SYMBOL_X = 3 SYMBOL_BOX = 4 SYMBOL_TRIANGLE = 5 SYMBOL_INVERTED_TRIANGLE = 6 SYMBOL_HEXAGON = 7 SYMBOL_SMALL_BOX = 8 SYMBOL_SMALL_DIAMOND = 9 SYMBOL_CIRCLE = 20 SYMBOL_3D_SPHERE = 0 SYMBOL_3D_CUBE = 1 SYMBOL_3D_CYLINDER = 2 SYMBOL_3D_CONE = 3 _weight_factor = (1.0 / 48.0, 1.0 / 24.0, 1.0 / 16.0, 1.0 / 12.0, 0.145, 1.0 / 4.0) FONT_WEIGHT_ULTRALIGHT = 1 FONT_WEIGHT_LIGHT = 2 FONT_WEIGHT_MEDIUM = 3 FONT_WEIGHT_BOLD = 4 FONT_WEIGHT_XBOLD = 5 FONT_WEIGHT_XXBOLD = 6 CMODEL_RGB = 0 CMODEL_CMY = 1 CMODEL_HSV = 2 C_BLACK = 67108863 C_RED = 33554687 C_GREEN = 33619712 C_BLUE = 50266112 C_CYAN = 50331903 C_MAGENTA = 50396928 C_YELLOW = 67043328 C_GREY = 41975936 C_LT_RED = 54542336 C_LT_GREEN = 54526016 C_LT_BLUE = 50348096 C_LT_CYAN = 50331712 C_LT_MAGENTA = 50348032 C_LT_YELLOW = 54525952 C_LT_GREY = 54542400 C_GREY10 = 51910680 C_GREY25 = 54542400 C_GREY50 = 41975936 C_WHITE = 50331648 C_TRANSPARENT = 0 REF_BOTTOM_LEFT = 0 REF_BOTTOM_CENTER = 1 REF_BOTTOM_RIGHT = 2 REF_CENTER_LEFT = 3 REF_CENTER = 4 REF_CENTER_RIGHT = 5 REF_TOP_LEFT = 6 REF_TOP_CENTER = 7 REF_TOP_RIGHT = 8 GROUP_ALL = 0 GROUP_MARKED = 1 GROUP_VISIBLE = 2 GROUP_AGG = 3 GROUP_CSYMB = 4 GROUP_VOXD = 5 LOCATE_FIT = gxapi.MVIEW_RELOCATE_FIT LOCATE_FIT_KEEP_ASPECT = gxapi.MVIEW_RELOCATE_ASPECT LOCATE_CENTER = gxapi.MVIEW_RELOCATE_ASPECT_CENTER COLOR_BAR_RIGHT = 0 COLOR_BAR_LEFT = 1 COLOR_BAR_BOTTOM = 2 COLOR_BAR_TOP = 3 COLOR_BAR_ANNOTATE_RIGHT = 1 COLOR_BAR_ANNOTATE_LEFT = -1 COLOR_BAR_ANNOTATE_TOP = 1 COLOR_BAR_ANNOTATE_BOTTOM = -1 CYLINDER_OPEN = 0 CYLINDER_CLOSE_START = 1 CYLINDER_CLOSE_END = 2 CYLINDER_CLOSE_ALL = 3 POINT_STYLE_DOT = 0 POINT_STYLE_SPHERE = 1 LINE3D_STYLE_LINE = 0 LINE3D_STYLE_TUBE = 1 LINE3D_STYLE_TUBE_JOINED = 2 SURFACE_FLAT = gxapi.MVIEW_DRAWOBJ3D_MODE_FLAT SURFACE_SMOOTH = gxapi.MVIEW_DRAWOBJ3D_MODE_SMOOTH _uom_attr = '/geosoft/data/unit_of_measure'
[docs]def face_normals_np(faces, verticies): """ Return normals of the verticies based on tringular faces, assuming right-hand winding of vertex for each face. :param faces: faces as array of triangle indexes into verticies, shaped (-1, 3) :param verticies: verticies as array of (x, y, z) shaped (-1, 3) :return: face normals shaped (-1, 3) The normal of a zero area face will be np.nan .. versionadded:: 9.3.1 """ tris = verticies[faces] n = np.cross(tris[::, 1] - tris[::, 0], tris[::, 2] - tris[::, 1]) return gxu.vector_normalize(n)
[docs]def vertex_normals_np(faces, verticies, normal_area=True): """ Return normals of the verticies based on tringular faces, assuming right-hand winding of vertex for each face. :param faces: faces as array of triangle indexes into verticies, shaped (-1, 3) :param verticies: verticies as array of (x, y, z) shaped (-1, 3) :param normal_area: True to weight normals by the area of the connected faces. :return: vertex normals shaped (-1, 3) .. versionadded:: 9.3.1 """ n = face_normals_np(faces, verticies) if not normal_area: n = gxu.vector_normalize(n) normals = np.zeros(verticies.shape, dtype=np.float64) normals[faces[:, 0]] += n normals[faces[:, 1]] += n normals[faces[:, 2]] += n return gxu.vector_normalize(normals)
[docs]def vertex_normals_vv(faces, verticies, normal_area=True): """ Return normals of the verticies based on tringular faces, assuming right-hand winding of vertex for each face. :param faces: (i1, i2, i3) `geosoft.gxpy.vv.GXvv` faces as array of triangle indexes into verticies :param verticies: (vx, vy, vz) `geosoft.gxpy.vv.GXvv` verticies :param normal_area: True to weight normals by the area of the connected faces. :return: (nx, ny, nz) `geosoft.gxpy.vv.GXvv` normals .. versionadded:: 9.3.1 """ faces = gxvv.np_from_vvset(faces) verticies = gxvv.np_from_vvset(verticies) n = vertex_normals_np(faces, verticies, normal_area=normal_area) return gxvv.GXvv(n[:, 0]), gxvv.GXvv(n[:, 1]), gxvv.GXvv(n[:, 2])
[docs]def color_from_string(cstr): """ Return a Geosoft color number from a color string. :param cstr: color string (see below) :returns: color Colour strings may be "R", "G", "B", "C", "M", "Y", "H", "S", "V", or "K" or a combination of these characters, each followed by up to three digits specifying a number between 0 and 255. An empty string will produce C_ANY_NONE. You must stay in the same color model, RGB, CMY, HSV or K. For example "R", "R127G22", "H255S127V32" Characters are not case sensitive. .. versionadded:: 9.3 """ return gxapi.GXMVIEW.color(str(cstr))
[docs]def edge_reference(area, reference): """ Location of a reference point of an area. :param area: :class:`Point2` instance, or (x0, y0, x1, y1) :param reference: reference point relative to the clip limits of the view to which reference location. The points are: :: 6 7 8 top left, center, right 3 4 5 middle left, center, right 0 1 2 bottom left, center, right :returns: Point desired reference location as a Point .. versionadded:: 9.2 """ if not isinstance(area, gxgm.Point2): area = gxgm.Point2(area) centroid = area.centroid half_dim = gxgm.Point(area.dimension) * 0.5 xoff = yoff = 0.0 if reference in (0, 1, 2): yoff = -half_dim.y elif reference in (6, 7, 8): yoff = half_dim.y if reference in (0, 3, 6): xoff = -half_dim.x elif reference in (2, 5, 8): xoff = half_dim.x return centroid + gxgm.Point((xoff, yoff))
[docs]class Group: """ Geosoft group class. :parameters: :view: gxpy.View :name: group name, default is "_". :plane: plane number, or plane name if drawing to a 3D view. Default is plane number 0. :view_lock: True to lock the view for a single-stream drawing group. Default is False. :unit_of_measure: unit of measurement for data in this group, default is '' :group_3d: True for a 3D drawing group, default assumes a 2D drawing group to a plane. :mode: `APPEND` (default), `NEW` or `READ_ONLY` :Properties: :view: the :class:`geosoft.gxpy.view.View` instance that contains this group :name: the name of the group :unit_of_measure: the unit of measurement (uom) for this data in this group :name_uom: uom decorated group name as it appears in a view :extent: extent of the group in view units :extent_map_cm: extent of the group in map cm :drawing_coordinate_system: the coordinate system of drawing coordinates. Setting to None will reset drawing coordinates to the view cs. If `drawing_coordinate_system` is set to some other cs the drawing coordinates will be transformed into the view cs. .. versionadded:: 9.2 .. versionchanged:: 9.3 added support for `unit_of_measure` .. versionchanged:: 9.3.1 added mode=REPLACE and changed mode=NEW to always create a new unique group. """ 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: try: self._drawing_coordinate_system = None self._pen = None self._text_def = None # write metadata if self._new_meta: bf = gxapi.GXBF.create("", gxapi.BF_READWRITE_NEW) try: self._meta.gxmeta.serial(bf) bf.seek(0, gxapi.BF_SEEK_START) self.view.gxview.write_group_storage(self.number, "Geosoft_META", bf) finally: del bf finally: self._view.lock = False self._view = None self._open = False self._meta = None self._new_meta = False def __repr__(self): return "{}({})".format(self.__class__, self.__dict__) def __str__(self): if self.view.is_3d and not self.group_3d: return '{}/{}/{}'.format(self.name, self.view.current_3d_drawing_plane, self.view.name) return '{}/{}'.format(self.name, self.view.name)
[docs] def __init__(self, view, name='_', plane=None, view_lock=False, mode=APPEND, unit_of_measure='', group_3d=False): if (len(name) == 0) or (name == view.name): name = name + '_' _lock = threading.Lock() _lock.acquire() try: if view.lock: raise GroupException(_t('This view is locked by group {}.'.format(view.lock))) if view_lock: view.lock = name finally: _lock.release() self.group_3d = False if view.is_3d: self.group_3d = group_3d if not group_3d: # setup a 2D drawing plane for this 2D group if plane is None: if view.current_3d_drawing_plane: plane = view.current_3d_drawing_plane else: plane = 'Plane' view.current_3d_drawing_plane = plane self._view = view self._name = name self._mode = mode self._new_meta = False self._meta = None if mode == REPLACE: if self.view.gxview.exist_group(name): self.view.delete_group(name) elif mode == NEW: # if the group exists, find a new unique group name if self.view.gxview.exist_group(name): self._name = gxu.unique_name(name, self.view.gxview.exist_group, separator='_') elif self.view.gxview.exist_group(self.name): group_number = self.view.gxview.find_group(self.name) if self.view.gxview.group_storage_exists(group_number, "Geosoft_META"): bf = self.view.gxview.read_group_storage(group_number, "Geosoft_META") if bf.size(): try: self._meta = gxmeta.Metadata(gxapi.GXMETA.create_s(bf)) finally: del bf if unit_of_measure: self.unit_of_measure = unit_of_measure self._view.gxview.start_group(self.name, mode) self._open = True
[docs] def close(self): """ Close the group, unlocks the view""" self._close()
@property def guid(self): """ The group GUID. .. versionadded:: 9.3 """ sr = gxapi.str_ref() self.view.gxview.get_group_guid(self.number, sr) return sr.value @property def view(self): """view that contains this group.""" return self._view @property def name(self): """group name""" return self._name @property def drawing_plane(self): """ drawing plane of this group, None for a group in a 2D view.""" if self.view.is_3d: return self.view.current_3d_drawing_plane else: return None @property def unit_of_measure(self): """ Unit of measure for scalar data contained in this group. This is only relevant for groups that contain scalar data, such as a Colour_symbols_group. For the spatial unit_of_measure use :attr:`drawing_coordinate_system.unit_of_measure` Can be set. ..versionadded:: 9.3 """ gxm = self.gx_metadata if gxm.has_attribute(_uom_attr): return gxm.get_attribute(_uom_attr) return '' @unit_of_measure.setter def unit_of_measure(self, uom): gxm = self.gx_metadata gxm.set_attribute(_uom_attr, str(uom)) self.gx_metadata = gxm @property def number(self): """group number in the view""" return self.view.gxview.find_group(self.name) def _extent(self, unit=UNIT_VIEW): xmin = gxapi.float_ref() ymin = gxapi.float_ref() xmax = gxapi.float_ref() ymax = gxapi.float_ref() self.view.gxview.get_group_extent(self.name, xmin, ymin, xmax, ymax, unit) return xmin.value, ymin.value, xmax.value, ymax.value @property def extent(self): """group extent as (xmin, ymin, xmax, ymax) in view units""" return self._extent(UNIT_VIEW) @property def visible(self): """True if group is visible, can be set.""" return self.name in self.view.group_list_visible @visible.setter def visible(self, visibility): if self.visible != visibility: marked = self.view.group_list_marked self.view.gxview.mark_all_groups(0) self.view.gxview.mark_group(self.name, 1) if visibility is True: self.view.gxview.hide_marked_groups(0) else: self.view.gxview.hide_marked_groups(1) self.view.gxview.mark_all_groups(0) for g in marked: self.view.gxview.mark_group(g, 1)
[docs] def extent_map_cm(self, extent=None): """ Return an extent in map cm. :param extent: an extent in view units as a tuple (xmin, ymin, xmax, ymax), Default is the group extent. .. versionadded:: 9.2 """ if extent is None: extent = self.extent xmin, ymin = self.view.view_to_map_cm(extent[0], extent[1]) xmax, ymax = self.view.view_to_map_cm(extent[2], extent[3]) return xmin, ymin, xmax, ymax
[docs] def locate(self, location, reference=REF_CENTER): """ Locate the group relative to a point. :param location: location (x, y) or a `geosoft.gxpy.geometry.Point` :param reference: reference point relative to the clip limits of the view to which reference location. The points are: :: 6 7 8 top left, center, right 3 4 5 center left, center, right 0 1 2 bottom left, center, right .. versionadded:: 9.2 """ area = gxgm.Point2(self.extent) area -= area.centroid area -= edge_reference(area, reference) area += location self.view.gxview.relocate_group(self.name, area.p0.x, area.p0.y, area.p1.x, area.p1.y, gxapi.MVIEW_RELOCATE_ASPECT_CENTER)
@property def gx_metadata(self): """ The group metadata as a Geosoft `geosoft.gxpy.metadata.Metadata` instance. This metadata may contain standard Geosoft metadata, such as unit_of_measure for data contained in the group, and you can add your own metadata spexific to your application. See `geosoft.gxpy.metadata.Metadata` for information about working with metadata. Can be set, in which case the metadata is replaced by the new metadata. Normally you will get the current metadata, add to or modify, then set it back. .. versionadded:: 9.3 """ if self._meta: return self._meta else: return gxmeta.Metadata() @gx_metadata.setter def gx_metadata(self, meta): self._new_meta = True self._meta = meta
def _draw(func): @wraps(func) def wrapper(self, *args, **kwargs): if self._mode == READ_ONLY: raise _t('This view is read-only.') if not self._pen: self._init_pen() if 'pen' in kwargs: cur_pen = self.pen try: self.pen = kwargs.pop('pen') func(self, *args, **kwargs) finally: self.pen = cur_pen else: func(self, *args, **kwargs) return wrapper def _make_point(p): if isinstance(p, gxgm.Point): return p else: return gxgm.Point(p) def _make_point2(p2): if isinstance(p2, gxgm.Point2): return p2 else: return gxgm.Point2(p2) def _make_ppoint(p): if isinstance(p, gxgm.PPoint): return p else: return gxgm.PPoint(p)
[docs]class Draw(Group): """ Create (start) a drawing group for 2D drawing elements. On a 3D view, 2D drawing elements are placed on the default drawing plane. Drawing groups will lock the view such that only one drawing group can be instantiated at a time. Use `with Draw() as group:` to ensure correct unlocking when complete. Inherits from the `Group` base class. See `Group` arguments. """
[docs] def __init__(self, *args, **kwargs): kwargs['view_lock'] = True super().__init__(*args, **kwargs) self._pen = None self._text_def = None self._drawing_coordinate_system = None if self._mode != READ_ONLY: self._init_pen() self._text_def = Text_def(factor=self.view.units_per_map_cm)
def _set_dot_symbol(self): # this is a hack because we cannot draw a box or a zero-length line, so # instead we draw a filled box self.view.gxview.symb_number(4) self.view.gxview.symb_color(0) self.view.gxview.symb_fill_color(self.pen.line_color.int_value) self.view.gxview.symb_size(self.pen.line_thick) @property def group_opacity(self): """ Group opacity setting. Can be set :return: opacity 0. to 1. (opaque) .. versionadded 9.3.1 """ fref = gxapi.float_ref() self.view.gxview.get_group_transparency(self.name, fref) return fref.value @group_opacity.setter def group_opacity(self, op): self.view.gxview.set_group_transparency(self.name, min(max(float(op), 0.), 1.)) @property def drawing_coordinate_system(self): """ The coordinate of incoming spatial data, which are converted to the coordinate system of the view. This is normally the same as the view coordinate system, but it can be set to a different coordinate system to have automatic reprojection occur during drawing. """ if self._drawing_coordinate_system is None: return self.view.coordinate_system return self._drawing_coordinate_system @drawing_coordinate_system.setter def drawing_coordinate_system(self, cs): if cs is None: self.view.gxview.set_user_ipj(self.view.coordinate_system.gxipj) self._drawing_coordinate_system = None else: self._drawing_coordinate_system = gxcs.Coordinate_system(cs) self.view.gxview.set_user_ipj(self._drawing_coordinate_system.gxipj) @property def pen(self): """the current drawing pen as a :class:`Pen` instance""" return self._pen @pen.setter def pen(self, pen): if self._mode == READ_ONLY: raise _t('This view is read-only.') if type(pen) is str: pen = Pen.from_mapplot_string(pen) if self._pen.line_color != pen.line_color: self.view.gxview.line_color(pen.line_color.int_value) if self._pen.line_thick != pen.line_thick: self.view.gxview.line_thick(pen.line_thick) if self._pen.line_smooth != pen.line_smooth: self.view.gxview.line_smooth(pen.line_smooth) if (self._pen.line_style != pen.line_style) or (self._pen.line_pitch != pen.line_pitch): self.view.gxview.line_style(pen.line_style, pen.line_pitch) if self._pen.fill_color != pen.fill_color: self.view.gxview.fill_color(pen.fill_color.int_value) if self._pen.pat_number != pen.pat_number: self.view.gxview.pat_number(pen.pat_number) if self._pen.pat_angle != pen.pat_angle: self.view.gxview.pat_angle(pen.pat_angle) if self._pen.pat_density != pen.pat_density: self.view.gxview.pat_density(pen.pat_density) if self._pen.pat_size != pen.pat_size: self.view.gxview.pat_size(pen.pat_size) if self._pen.pat_style != pen.pat_style: self.view.gxview.pat_style(pen.pat_style) if self._pen.pat_thick != pen.pat_thick: self.view.gxview.pat_thick(pen.pat_thick) self._pen = pen def _init_pen(self): scm = self.view.units_per_map_cm pen = Pen(line_thick=0.02 * scm, line_pitch=0.5 * scm, pat_size=0.25 * scm, pat_thick=0.02 * scm) self.view.gxview.line_color(pen.line_color.int_value) self.view.gxview.line_thick(pen.line_thick) self.view.gxview.line_smooth(pen.line_smooth) self.view.gxview.line_style(pen.line_style, pen.line_pitch) self.view.gxview.fill_color(pen.fill_color.int_value) self.view.gxview.pat_number(pen.pat_number) self.view.gxview.pat_angle(pen.pat_angle) self.view.gxview.pat_density(pen.pat_density) self.view.gxview.pat_size(pen.pat_size) self.view.gxview.pat_style(pen.pat_style) self.view.gxview.pat_thick(pen.pat_thick) self._pen = pen
[docs] def new_pen(self, **kwargs): """ Returns a pen that inherits default from the current view pen. Arguments are the same as the `Pen` constructor. This using this ensures that default sizing of view unit-based dimensions (such as `line_thick`) are not lost when new pens are created. :param kwargs: see :class:`Pen` :returns: :class:`Pen` instance .. versionadded:: 9.2 """ return Pen(default=self.pen, **kwargs)
@property def text_def(self): """the current text definition as a :class:`Text_def` instance, can be set.""" return self._text_def @text_def.setter def text_def(self, text_def): if self._mode == READ_ONLY: raise _t('This view is read-only.') if self._text_def != text_def: self._text_def = text_def self.view.gxview.text_font(text_def.font, text_def.gfn, text_def.weight, text_def.italics) self.view.gxview.text_size(text_def.height) self.view.gxview.text_color(text_def.color.int_value)
[docs] def text_extent(self, str, text_def=None): """ Return the extent of a text string in view units relative to the current text `text_def` setting, or the specified `text_def` setting. :param str: text string :param text_def: `text_def` instance, None for the current setting :return: `geosoft.geometry.Point2` instance .. versionadded:: 9.4 """ x0 = gxapi.float_ref() y0 = gxapi.float_ref() x1 = gxapi.float_ref() y1 = gxapi.float_ref() if text_def: cur_text = self._text_def self.text_def = text_def else: cur_text = None self.view.gxview.measure_text(str, x0, y0, x1, y1) if cur_text: self.text_def = cur_text return gxgm.Point2(((x0.value, y0.value), (x1.value, y1.value)), coordinate_system=self.view.coordinate_system)
[docs] @_draw def point(self, p): """ Draw a point. :param p: point location as `geosoft.gxpy.geometry.Point` .. versionadded:: 9.3 """ # just draw a box. TODO: MVIEW needs a way to draw a dot, and/or address issue #44 self._set_dot_symbol() self.view.gxview.symbol(p.x, p.y)
[docs] @_draw def polypoint(self, pp): """ Draw many points. :param pp: point location as `geosoft.gxpy.geometry.PPoint`, or a pair of VVs (vvx, vvy), or something that `gxpy.geometry.PPoint` can construct into a PP. .. versionadded:: 9.3 """ self._set_dot_symbol() if not((len(pp) == 2) and isinstance(pp[0], gxvv.GXvv)): pp = _make_ppoint(pp) pp = (gxvv.GXvv(pp.x), gxvv.GXvv(pp.y)) self.view.gxview.symbols(pp[0].gxvv, pp[1].gxvv)
[docs] @_draw def line(self, p2): """ Draw a line on the current plane :param p2: :class:`geometry.Point2`, or (p1, p2) .. versionadded:: 9.2 """ p2 = _make_point2(p2) self.view.gxview.line(p2.p0.x, p2.p0.y, p2.p1.x, p2.p1.y)
[docs] @_draw def polyline(self, pp, close=False): """ Draw a polyline the current plane :param pp: `geosoft.gxpy.geometry.PPoint` instance or something that can be constructed, or a pair of `geosoft.gxpy.vv.GXvv` (xvv, yvv) :param close: if True, draw a polygon, default is a polyline .. note:: Smooth-line polygons must have at least 6 points for the closure to appear continuous. .. versionadded:: 9.2 """ if not((len(pp) == 2) and isinstance(pp[0], gxvv.GXvv)): pp = _make_ppoint(pp) pp = (gxvv.GXvv(pp.x), gxvv.GXvv(pp.y)) if close: self.view.gxview.poly_line(gxapi.MVIEW_DRAW_POLYGON, pp[0].gxvv, pp[1].gxvv) else: self.view.gxview.poly_line(gxapi.MVIEW_DRAW_POLYLINE, pp[0].gxvv, pp[1].gxvv)
[docs] @_draw def polygon(self, pp): """ Draw a polygon on the current plane. :param pp: :class:`geosoft.gxpy.geometry.PPoint` .. note:: Smooth-line polygons must have at least 6 points for the closure to appear continuous. .. versionadded:: 9.2 """ self.polyline(pp, True)
[docs] @_draw def rectangle(self, p2): """ Draw a 2D rectangle on the current plane :param p2: geometry.Point2, or (p1, p2), or (x0, y0, x2, y2) .. versionadded:: 9.2 """ p2 = _make_point2(p2) self.view.gxview.rectangle(p2.p0.x, p2.p0.y, p2.p1.x, p2.p1.y)
[docs] @_draw def graticule(self, dx=None, dy=None, ddx=None, ddy=None, style=GRATICULE_LINE): """ Draw a graticule reference on a view. :param style: `GRATICULE_LINE`, `GRATICULE_CROSS` or `GRATICULE_DOT` :param dx: vertical line separation :param dy: horizontal line separation :param ddx: horizontal cross size for `GRATICULE_CROSS` :param ddy: vertical cross size for `GRATICULE_CROSS` .. versionadded:: 9.2 """ ext = self.extent if dx is None: dx = (ext[2] - ext[0]) * 0.2 ddx = dx * 0.25 if dy is None: dy = (ext[3] - ext[1]) * 0.2 ddy = dy * 0.25 if ddy is None: ddy = dy * 0.25 if ddx is None: ddx = dx * 0.25 self.view.gxview.grid(dx, dy, ddx, ddy, style)
[docs] def text(self, text, location=(0, 0), reference=REF_BOTTOM_LEFT, angle=0., text_def=None): """ Draw text in the view. :param text: text string. Use line-feed characters for multi-line text. :param location: (x, y) or a `gxpy.geomerty.Point` location :param reference: Text justification point relative text outline box. The points are: :: 6 7 8 top left, center, right 3 4 5 middle left, center, right 0 1 2 bottom left, center, right :param angle: baseline angle in degrees clockwise :param text_def: text definition, if not set the current definition is used .. versionadded:: 9.2 """ if text_def: cur_text = self._text_def self.text_def = text_def else: cur_text = None self.view.gxview.text_ref(reference) self.view.gxview.text_angle(angle) if not isinstance(location, gxgm.Point): location = gxgm.Point(location) self.view.gxview.text(text, location.x, location.y) if cur_text: self.text_def = cur_text
[docs] def contour(self, grid_file_name, parameters=None): """ Draw contours for a grid file. :param grid_file_name: Grid file name :param parameters: contour parameters, None for default contouring. Parameters can be provided as a list of strings that correspond the contouring control file (starting at control file line 2) as defined on the Geosoft Desktop help topic 'CONTCON'. The first 'MDF' line, which is used to specify the map scale and drawing location, is not required as the scale and location is fixed by the view. Following are the control file parameters names as they would appear in a text control file: .. code:: border, lowtic, smooth, suppop, nth, gtitle / 'general': {} pdef, ptick, pxval, pframe, psidel / 'special': {} hlb, htl, hcr, htk, hxv, hsl / 'text': {} ominl,cminl,skipa,skipb,skipc,skipd,skipe,skipf / 'line': {} xval, digxv, minxv, markxv / 'hilo': {} levopt, conbeg, conend, lbup, lbmult, logopt / 'levels': {} cint,lintyp,catt,label,dense,digits,conlo,conhi / 'contours': [{}, {}, ...] ... ... up to 32 contour levels ... Example parameters as text strings: ====================================== ================================================================= `parameter=` **Outcome** `None` default contour based on the grid data range `('','','','','','','10')` multiples of 10 `('','','','','','','10','50','250')` multiples of 10, 50 and 100, default attributes `('','','','','','0','0,,,0')` single contour (levopt=0) at value 0 (cint=0), no label (label=0) `('','','','','','0','0,,a=rt500,0')` red 500 micron thick contour at value 0, no label ====================================== ================================================================= Parameters may also be defined in a dictionary using explicit parameter names as shown in the text control file template above. Each line of parameters is defined by the key name to the right on the `/`, and the 'contours' entry is a list, and the values are dictionaries of the parameters to be defines. Parameters that are not defined will have the documented default behaviour. Example parameters as a dictionary: ================================================= =========================================== `parameter=` **Outcome** ------------------------------------------------- ------------------------------------------- `{'contours':[{'cint':10}]}` multiples of 10 `{'contours':[{'cint':10},{cint':50}]}` multiples of 10 and 50, default attributes `{'levels':{'levopt':0},[{'cint':10,'label':0}]}` single contour at data value = 0, no label ================================================= =========================================== .. versionadded:: 9.2 .. versionadded:: 9.4 added parameter controls """ def parms(set_str, keys): pstr = '' items = keys.split(',') if len(set_str): for k in items: pstr = pstr + str(set_str.get(k.strip(), '')) + ',' pstr = pstr[:-1] + ' /' + keys return pstr scale, ufac, x0, y0 = self.view.mdf()[1] control_file = gx.gx().temp_file('.con') with open(control_file, 'w+') as f: f.write('{},{},{},{} /scale, ufac, x0, y0\n'.format(scale, ufac, x0, y0)) if parameters is None: f.write(',,-1/\n') elif isinstance(parameters, dict): f.write('{}\n'.format(parms(parameters.get('general', {}), 'border, lowtic, smooth, suppop, nth, gtitle'))) f.write('{}\n'.format(parms(parameters.get('special', {}), 'pdef, ptick, pxval, pframe, psidel'))) f.write('{}\n'.format(parms(parameters.get('text', {}), 'hlb, htl, hcr, htk, hxv, hsl'))) f.write('{}\n'.format(parms(parameters.get('line', {}), 'ominl, cminl, skipa, skipb, skipc, skipd, skipe, skipf'))) f.write('{}\n'.format(parms(parameters.get('hilo', {}), 'xval, digxv, minxv, markxv'))) f.write('{}\n'.format(parms(parameters.get('levels', {}), 'levopt, conbeg, conend, lbup, lbmult, logopt'))) contours = parameters.get('contours', []) if len(contours) == 0: raise GroupException(_t('No contour levels specified.')) for con in contours: f.write('{}\n'.format(parms(con, 'cint, lintyp, catt, label, dense, digits, conlo, conhi'))) else: for pline in parameters: f.write(pline + '\n') geosoft.gxapi.GXMVU.contour(self.view.gxview, control_file, grid_file_name) gxu.delete_file(control_file)
[docs]class Draw_3d(Draw): """ Create a 3D drawing group within a 3D view. 3D drawing groups accept 3D drawing objects that can be created using methods of this class. 2D objects can also be drawn to a 3D group and will be placed on the default drawing plane within the 3D view. :param render_backfaces: True to turn backface rendering on. .. versionadded:: 9.2 """
[docs] def __init__(self, view, *args, render_backfaces=False, **kwargs): if not isinstance(view, gxv.View_3d): raise GroupException(_t('View is not 3D')) kwargs['group_3d'] = True super().__init__(view, *args, **kwargs) if render_backfaces: self.render_backfaces = True
@property def render_backfaces(self): """ True if backface rendering is on, default is off (False). Backface rendering controls the rendering of parts of solid objects that would normally be hidden from view. If drawing solid objects that have an open face, such as cylinders with an open end, backface rendering will be be turned on. Once on it cannot be turned off for a view. .. versionadded:: 9.2 """ return bool(self.view.gxview.get_3d_group_flags(self.number) & 0b1) @render_backfaces.setter def render_backfaces(self, setting): if not setting and self.render_backfaces: raise GroupException(_t('Once backface rendering is on it cannot be turned off.')) if not self.render_backfaces: f3d = (self.view.gxview.get_3d_group_flags(self.number) & 0b11111110) | 0b1 self.view.gxview.set_3d_group_flags(self.number, f3d)
[docs] @_draw def sphere(self, p, radius): """ Draw a sphere. :param p: location as geometry.Point, or (x, y, z) :param radius: sphere radius .. versionadded:: 9.2 """ # solids use the fill color as the object color fci = self.pen.fill_color.int_value self.view.gxview.fill_color(self.pen.line_color.int_value) try: p = _make_point(p) self.view.gxview.sphere_3d(p.x, p.y, p.z, radius) finally: self.view.gxview.fill_color(fci) self.view.add_extent(gxgm.Point2((p - radius, p + radius)))
[docs] @_draw def box_3d(self, p2, wireframe=False): """ Draw a 3D box :param p2: box corners as geometry.Point2, or (p0, p1), or (x0, y0, z0, x1, y1, z1) :param wireframe: True to draw edges only .. versionadded:: 9.2 """ # solids use the fill color as the object color fci = self.pen.fill_color.int_value self.view.gxview.fill_color(self.pen.line_color.int_value) pp = _make_point2(p2) try: if wireframe: sq = gxgm.PPoint(((pp.p0.x, pp.p0.y, pp.p0.z), (pp.p0.x, pp.p1.y, pp.p0.z), (pp.p1.x, pp.p1.y, pp.p0.z), (pp.p1.x, pp.p0.y, pp.p0.z), (pp.p0.x, pp.p0.y, pp.p0.z))) self.polyline_3d(sq, style=LINE3D_STYLE_TUBE_JOINED) sq += (0, 0, pp.p1.z - pp.p0.z) self.polyline_3d(sq, style=LINE3D_STYLE_TUBE_JOINED) self.cylinder_3d(gxgm.Point2(((pp.p0.x, pp.p0.y, pp.p0.z), (pp.p0.x, pp.p0.y, pp.p1.z))), radius=self.pen.line_thick * 0.5) self.cylinder_3d(gxgm.Point2(((pp.p0.x, pp.p1.y, pp.p0.z), (pp.p0.x, pp.p1.y, pp.p1.z))), radius=self.pen.line_thick * 0.5) self.cylinder_3d(gxgm.Point2(((pp.p1.x, pp.p1.y, pp.p0.z), (pp.p1.x, pp.p1.y, pp.p1.z))), radius=self.pen.line_thick * 0.5) self.cylinder_3d(gxgm.Point2(((pp.p1.x, pp.p0.y, pp.p0.z), (pp.p1.x, pp.p0.y, pp.p1.z))), radius=self.pen.line_thick * 0.5) else: self.view.gxview.box_3d(pp.p0.x, pp.p0.y, pp.p0.z, pp.p1.x, pp.p1.y, pp.p1.z) finally: self.view.gxview.fill_color(fci) self.view.add_extent(pp.extent)
[docs] @_draw def cylinder_3d(self, p2, radius, r2=None, close=CYLINDER_CLOSE_ALL): """ Draw a cylinder. :param p2: end points as geometry.Point2, or (p0, p1), or (x0, y0, z0, x1, y1, z1) :param radius: cylinder radius. :param r2: end radius if different from the start :param close: one of: :: CYLINDER_OPEN CYLINDER_CLOSE_START CYLINDER_CLOSE_END CYLINDER_CLOSE_ALL .. versionadded:: 9.2 """ # solids use the fill color as the object color fci = self.pen.fill_color.int_value self.view.gxview.fill_color(self.pen.line_color.int_value) if close != CYLINDER_CLOSE_ALL: self.render_backfaces = True try: p2 = _make_point2(p2) if r2 is None: r2 = radius self.view.gxview.cylinder_3d(p2.p0.x, p2.p0.y, p2.p0.z, p2.p1.x, p2.p1.y, p2.p1.z, radius, r2, close) finally: self.view.gxview.fill_color(fci) r = max(radius, r2) ext = p2.extent self.view.add_extent(gxgm.Point2((ext.p0 - r, ext.p1 + r)))
[docs] @_draw def cone_3d(self, p2, radius): """ Draw a cone. :param p2: end points as geometry.Point2, or (p0, p1), or (x0, y0, z0, x1, y1, z1). :param radius: cone base radius, base is as the the first point of p2. .. versionadded:: 9.2 """ self.cylinder_3d(p2, radius, r2=0.)
def _poly_3d(self, points, ptype, smooth=gxapi.MVIEW_DRAWOBJ3D_MODE_FLAT): vvx, vvy, vvz = points.make_xyz_vv() null_vv = gxapi.GXVV.null() self.view.gxview.draw_object_3d(ptype, smooth, vvx.length, 0, vvx.gxvv, vvy.gxvv, vvz.gxvv, null_vv, null_vv, null_vv, null_vv, null_vv, null_vv)
[docs] @_draw def polypoint_3d(self, points, style=POINT_STYLE_DOT): """ Draw multiple points. :param points: points to draw, :class:`geosoft.gxpy.geometry.PPoint` instance, or array-like [x,y,z] :param style: POINT_STYLE_DOT or POINT_STYLE_SPHERE. Dots are fast and intended for point clouds. The current pen thickness is used as the sphere sizes. .. versionadded:: 9.2 """ points = _make_ppoint(points) radius = self.pen.line_thick * 0.5 if style == POINT_STYLE_DOT: self._poly_3d(points, gxapi.MVIEW_DRAWOBJ3D_ENTITY_POINTS) else: for i in range(points.length): self.sphere(points[i], radius=radius) ext = points.extent self.view.add_extent(gxgm.Point2((ext.p0 - radius, ext.p1 + radius)))
[docs] @_draw def polyline_3d(self, points, style=LINE3D_STYLE_LINE): """ Draw a polyline. :param points: verticies of the polyline, :class:`geosoft.gxpy.geometry.PPoint` instance, or array-like [x,y,z] :param style: LINE3D_STYLE_LINE, LINE3D_STYLE_TUBE or LINE3D_STYLE_TUBE_JOINED. Lines are single-pixel-wide. Tubes have width defined by the pen line thickness. Joined tubes have a joints and rounded ends. .. versionadded:: 9.2 """ points = _make_ppoint(points) if points.length < 2: raise GroupException(_t('Need at least two points.')) radius = self.pen.line_thick * 0.5 if style == LINE3D_STYLE_LINE: vvx, vvy, vvz = points.make_xyz_vv() self.view.gxview.poly_line_3d(vvx.gxvv, vvy.gxvv, vvz.gxvv) else: self.pen = Pen(fill_color=self.pen.line_color, default=self.pen) for i in range(points.length-1): self.cylinder_3d(gxgm.Point2((points[i], points[i+1])), radius=radius) if style == LINE3D_STYLE_TUBE_JOINED: for i in range(points.length): self.sphere(points[i], radius=radius) ext = points.extent self.view.add_extent(gxgm.Point2((ext.p0 - radius, ext.p1 + radius)))
[docs] def polydata_3d(self, data, render_info_func=None, passback=None): """ Create 3D objects rendered using data attributes. :param data: iterable that yields items passed to your `render_info_func` callback :param render_info_func: a callback that given `(item, passback)` returns the rendering `(symbol_type, geometry, color_integer, attribute)`: ================== ======== =============== ========= Symbol Geometry Color Attribute ================== ======== =============== ========= SYMBOL_3D_SPHERE Point Color.int_value radius SYMBOL_3D_CUBE Point2 Color.int_value None SYMBOL_3D_CYLINDER Point2 Color.int_value radius SYMBOL_3D_CONE Point2 Color.int_value radius ================== ======== =============== ========= :param passback: something passed back to your render_info_func function, default None. **Example** .. code:: import geosoft.gxpy.geometry as gxgm import geosof.gxpy.view as gxv import geosogt.gxpy.group as gxg def render_spheres(xyz, cmap_radius): color, radius = cmap_radius return gxg.SYMBOL_3D_SPHERE, xyz, color.int_value, radius data = gxgm.PPoint(((5, 5, 5), (7, 5, 5), (7, 7, 7))) with gxv.View_3d.new('example_polydata') as v: with gxg.Draw_3d(v, 'red_spheres') as g: g.polydata_3d(data, render_spheres, (gxg.Color('r'), 0.25)) .. versionadded:: 9.2 """ cint = None for item in data: render = render_info_func(item, passback) if render: symbol, geometry, color, attribute = render if color != cint: self.view.gxview.fill_color(color) cint = color if symbol == SYMBOL_3D_SPHERE: self.view.gxview.sphere_3d(geometry[0], geometry[1], geometry[2], attribute) if not isinstance(geometry, gxgm.Geometry): geometry = gxgm.Point(geometry) elif symbol == SYMBOL_3D_CUBE: self.view.gxview.box_3d(geometry.p0.x, geometry.p0.y, geometry.p0.z, geometry.p1.x, geometry.p1.y, geometry.p1.z) elif symbol == SYMBOL_3D_CYLINDER: self.view.gxview.cylinder_3d(geometry.p0.x, geometry.p0.y, geometry.p0.z, geometry.p1.x, geometry.p1.y, geometry.p1.z, attribute, attribute, CYLINDER_CLOSE_ALL) elif symbol == SYMBOL_3D_CONE: self.view.gxview.cylinder_3d(geometry.p0.x, geometry.p0.y, geometry.p0.z, geometry.p1.x, geometry.p1.y, geometry.p1.z, attribute, 0, CYLINDER_CLOSE_ALL) else: raise GroupException(_t('Symbol type not implemented')) if attribute: e = gxgm.Point2(geometry).extent self.view.add_extent((e.p0 - attribute, e.p1 + attribute)) else: self.view.add_extent(geometry.extent)
def _surface(self, faces, verticies, coordinate_system=None): """ TODO: awaiting resolution of #73 Draw a surface defined by faces and verticies :param faces: triangle faces as indexes into verticies, numpy array (n_faces, 3) :param verticies: verticies, numpy array (n_verticies, 3) :param coordinate_system: `geosoft.gxpy.Coordinate_system` instance if not in the drawing CS. .. versionadded:: 9.3.1 """ n_faces = len(faces) n_verticies = len(verticies) if np.nanmax(faces) > n_verticies or np.nanmin(faces) < 0: raise GroupException(_t('Faces refer to verticies out of range of verticies.')) # TODO validate buffering and rendering performance once #73 is resolved. n_buff = 1000 n_faces_written = 0 # normals normals = vertex_normals_np(faces, verticies) # coordinate_system if isinstance(coordinate_system, gxcs.Coordinate_system): gxipj = coordinate_system.gxipj else: gxipj = self.drawing_coordinate_system.gxipj # TODO: implement variable colour once issue #73 is addressed # color color = self.pen.fill_color.int_value if color == 0: color = C_GREY self.render_backfaces = True while n_faces_written < n_faces: n_write = min(n_buff, n_faces - n_faces_written) n_last = n_faces_written + n_write faces_buff = faces[n_faces_written: n_last] verticies_buff = verticies[faces_buff].reshape(-1, 3) vx, vy, vz = gxvv.vvset_from_np(verticies_buff) vf1, vf2, vf3 = gxvv.vvset_from_np(faces_buff) nx, ny, nz = gxvv.vvset_from_np(normals[faces_buff].reshape(-1, 3)) self.view.gxview.draw_surface_3d_ex(self.name, vx.gxvv, vy.gxvv, vz.gxvv, nx.gxvv, ny.gxvv, nz.gxvv, gxapi.GXVV.null(), color, vf1.gxvv, vf2.gxvv, vf3.gxvv, gxipj) n_faces_written += n_write
[docs]def surface_group_from_file(v3d, file_name, group_name=None, overwrite=False): """ Create a 3D surface group from a surface dataset file. :param v3d: `geosoft.gxpy.view.View_3d` instance :param file_name: surface dataset file name (extension .geosoft_surface). See `geosoft.gxpy.surface.SurfaceDataset`. :param group_name: group name, default is the base file name. :param overwrite: True to overwrite existing group .. versionadded:: 9.3.1 """ if group_name is None: group_name = os.path.basename(file_name) group_name = os.path.splitext(group_name)[0] if v3d.has_group(group_name) and not overwrite: raise GroupException(_t('Cannot overwrite exing group "{}"').format(group_name)) v3d.gxview.draw_surface_3d_from_file(group_name, file_name) ext = gxspd.extent_from_metadata_file(file_name) v3d.add_extent(ext)
[docs]def contour(view, group_name, grid_file_name, parameters=None): """ Create a contour group from a grid file. A default contour interval is determined from the grid. :param view: `geosoft.gxpy.view.View` instance :param group_name: name for the contour group :param grid_file_name: Grid file name .. versionadded:: 9.3 """ with Draw(view, group_name) as g: g.contour(grid_file_name, parameters=parameters)
[docs]def legend_color_bar(view, group_name, cmap, cmap2=None, bar_location=COLOR_BAR_RIGHT, location=None, decimals=None, annotation_height=0.2, annotation_offset=None, annotation_side=COLOR_BAR_ANNOTATE_RIGHT, box_size=None, bar_width=None, max_bar_size=None, minimum_gap=0, post_end_values=False, annotate_vertical=False, division_line=1, interval_1=None, interval_2=None, title=None): """ Draw a color bar legend from :class:Color_map coloring definitions. :param view: :class:`gxpy.view.View` instance in which to place the bar :param group_name: name for the color_bar group, overwrites group if it exists. :param cmap: :class:`Color_map` instance :param cmap2: optional orthogonal blended :class:`Color_map` instance. If making a shaded-color legend, provide the shaded color map here. :param bar_location: one of: :: COLOR_BAR_RIGHT = 0 COLOR_BAR_LEFT = 1 COLOR_BAR_BOTTOM = 2 COLOR_BAR_TOP = 3 :param location: offset or (x, y) offset from `bar_location` reference point, in cm. The default is determined to center the bar off the location side specified. :param decimals: annotation decimal places :param annotation_height: annotation number height (cm) :param annotation_offset: offset of annotations from the bar (cm) :param annotation_side: side of the bar for annotations :: COLOR_BAR_ANNOTATE_RIGHT = 1 COLOR_BAR_ANNOTATE_LEFT = -1 COLOR_BAR_ANNOTATE_TOP = 1 COLOR_BAR_ANNOTATE_BOTTOM = -1 :param box_size: box size, height for vertical bars, width for horizontal bars :param bar_width: width of the color boxes, horizontal for vertical bars, vertical for horizontal bars :param max_bar_size: maximum bar size, default is the size of the view edge :param minimum_gap: minimum gap to between annotations. Annotations are dropped in necessary. :param post_end_values: post the maximum and minimum values :param annotate_vertical: True to orient labels vertically :param division_line: 0, no division lines, 1 - line, 2 - tick :param interval_1: Major annotation increment, default annotates everything :param interval_2: secondary smaller annotations, reduced to 1/10, 1/5, 1/4 or 1/2 of interval_1. Default chooses something reasonable. :param title: bar title, use new-lines for sub-titles. Default uses the title and unit_of_measure from `cmap`. .. versionadded:: 9.2 """ # ensure group name is unique in the view while group_name in view.group_list: group_name += '_' # default decimals if decimals is None: decimals = 1 minz = maxz = cmap.color_map[0][0] for c in cmap.color_map: z = c[0] if z: if z < minz: minz = z elif z > maxz: maxz = z delta = maxz - minz while delta > 0 and delta < 100: delta *= 10. decimals += 1 itr = cmap.gxitr with Draw(view, group_name) as g: v_area = gxgm.Point2(view.extent_clip) v_width = v_area.dimension[0] v_height = v_area.dimension[1] if (bar_location == COLOR_BAR_LEFT) or (bar_location == COLOR_BAR_RIGHT): bar_orient = 0 default_bar_size = v_height * 0.8 if max_bar_size is None: max_bar_size = v_height else: bar_orient = 1 default_bar_size = v_width * 0.8 if max_bar_size is None: max_bar_size = v_width * 0.8 # bar cell sizing def_box_size = default_bar_size / itr.get_size() if box_size is None: box_size = min(0.4 * view.units_per_map_cm, def_box_size) else: box_size *= view.units_per_map_cm if bar_width is None: if bar_location in (COLOR_BAR_LEFT, COLOR_BAR_RIGHT): bar_width = max(0.4 * view.units_per_map_cm, box_size * 2.0) else: bar_width = max(0.4 * view.units_per_map_cm, box_size) else: bar_width *= view.units_per_map_cm if max_bar_size is not None: box_size = min(box_size, max_bar_size / itr.get_size()) annotation_height *= view.units_per_map_cm if annotation_offset is None: annotation_offset = annotation_height * 0.5 else: annotation_offset *= view.units_per_map_cm annotation_offset *= annotation_side minimum_gap *= view.units_per_map_cm cdict = { "BAR_ORIENTATION": bar_orient, "DECIMALS": decimals, 'ANNOFF': annotation_offset, 'BOX_SIZE': box_size, 'BAR_WIDTH': bar_width, 'MINIMUM_GAP': minimum_gap, "X": v_area.centroid.x, "Y": v_area.centroid.y, "POST_MAXMIN": 1 if post_end_values else 0, "LABEL_ORIENTATION": 0 if annotate_vertical else 1, "DIVISION_STYLE": division_line, } if interval_1: if interval_2 is None: interval_2 = gxapi.rDUMMY if interval_2 <= interval_1 / 10.: interval_2 = interval_1 / 10. elif interval_2 <= interval_1 / 5.: interval_2 = interval_1 / 5. elif interval_2 <= interval_1 / 4.: interval_2 = interval_1 / 4. elif interval_2 <= interval_1 / 2.: interval_2 = interval_1 / 2. else: interval_2 = gxapi.rDUMMY cdict["FIXED_INTERVAL"] = interval_1 cdict["FIXED_MINOR_INTERVAL"] = interval_2 g.text_def = Text_def(height=annotation_height) if cmap2 is None: itr2 = gxapi.GXITR.null() else: itr2 = cmap2.gxitr gxapi.GXMVU.color_bar_reg(view.gxview, itr, itr2, gxu.reg_from_dict(cdict, 100, json_encode=False)) if title is None: if cmap.unit_of_measure: title = '{}\n({})'.format(cmap.title, cmap.unit_of_measure) else: title = cmap.title if title: title_height = annotation_height * 1.5 g.text_def = Text_def(height=title_height, weight=FONT_WEIGHT_BOLD) p = gxgm.Point(edge_reference(gxgm.Point2(g.extent), REF_BOTTOM_CENTER)) p -= (0, title_height * 0.5) if '\n' in title: tline = title[:title.index('\n')] title = title[title.index('\n') + 1:] else: tline = title title = '' g.text(tline, p, reference=REF_TOP_CENTER) if title: g.text_def = Text_def(height=title_height * 0.8, weight=FONT_WEIGHT_LIGHT) p -= (0, title_height * 1.5) g.text(title, p, reference=REF_TOP_CENTER) # locate the bar default_offset = 1.5 * view.units_per_map_cm if location and (not hasattr(location, '__iter__')): default_offset = location * view.units_per_map_cm location = None if location is not None: location = location[0] * view.units_per_map_cm, location[1] * view.units_per_map_cm area = gxgm.Point2(view.extent_clip) if bar_location == COLOR_BAR_LEFT: if location is None: location = (-default_offset, 0) xy = edge_reference(area, REF_CENTER_LEFT) reference = REF_CENTER_RIGHT elif bar_location == COLOR_BAR_BOTTOM: if location is None: location = (0, -default_offset) xy = edge_reference(area, REF_BOTTOM_CENTER) reference = REF_TOP_CENTER elif bar_location == COLOR_BAR_TOP: if location is None: location = (0, default_offset) xy = edge_reference(area, REF_TOP_CENTER) reference = REF_BOTTOM_CENTER else: # BAR_RIGHT if location is None: location = (default_offset, 0) xy = edge_reference(area, REF_CENTER_RIGHT) reference = REF_CENTER_LEFT location = xy + location g.locate(location, reference)
[docs]class Color: """ Colours, which are stored as a 32-bit color integer. :param color: string descriptor (eg. 'R255G0B125'), color letter R, G, B, C, M, Y, H, S or V.; tuple (r, g, b), (c, m, y) or (h, s, v), each item defined in the range 0 to 255; 32-bit color number, which can be an item selected from the following list: :: C_BLACK C_RED C_GREEN C_BLUE C_CYAN C_MAGENTA C_YELLOW C_GREY C_LT_RED C_LT_GREEN C_LT_BLUE C_LT_CYAN C_LT_MAGENTA C_LT_YELLOW C_LT_GREY C_GREY10 C_GREY25 C_GREY50 C_WHITE C_TRANSPARENT :param model: model of the tuple: :: CMODEL_RGB (default) CMODEL_CMY CMODEL_HSV .. versionadded:: 9.2 """
[docs] def __init__(self, color, model=CMODEL_RGB): if isinstance(color, Color): self._color = color.int_value elif isinstance(color, int): self.int_value = color elif isinstance(color, str): self._color = gxapi.GXMVIEW.color(color) else: if model == CMODEL_CMY: self.cmy = color elif model == CMODEL_HSV: hue = max(0, min(255, color[0])) sat = max(0, min(255, color[1])) val = max(0, min(255, color[2])) self._color = gxapi.GXMVIEW.color_hsv(hue, sat, val) else: self.rgb = color
def __eq__(self, other): return self.int_value == other.int_value def __ne__(self, other): return not self.__eq__(other) @property def int_value(self): """ color as a 32-bit color integer, can be set""" return self._color @int_value.setter def int_value(self, color): if color < 0: raise GroupException(_t('Invalid color integer {}, must be >= 0').format(color)) self._color = int(color) @property def rgb(self): """color as an (red, green, brue) tuple, can be set""" if self.int_value == 0: return None r = gxapi.int_ref() g = gxapi.int_ref() b = gxapi.int_ref() gxapi.GXMVIEW.color2_rgb(self._color, r, g, b) return r.value, g.value, b.value @rgb.setter def rgb(self, rgb): r = max(min(255, rgb[0]), 0) g = max(min(255, rgb[1]), 0) b = max(min(255, rgb[2]), 0) self._color = gxapi.GXMVIEW.color_rgb(r, g, b) @property def cmy(self): """color as an (cyan, magenta, yellow) tuple, can be set""" if self.int_value == 0: return None red, green, blue = self.rgb return 255 - red, 255 - green, 255 - blue @cmy.setter def cmy(self, cmy): self.rgb = (255 - cmy[0], 255 - cmy[1], 255 - cmy[2])
[docs] def adjust_brightness(self, brightness): """ Return a :class:`Color` instance adjusted for brightness. .. versionadded:: 9.2 """ if brightness == 0.: return self c, m, y = self.rgb if brightness > 0.0: w = round(brightness * 255) c = max(c - w, 0) m = max(m - w, 0) y = max(y - w, 0) return Color((c, m, y), model=CMODEL_CMY) else: k = round(-brightness * 255) c = max(c + k, 255) m = max(m + k, 255) y = max(y + k, 255) return Color((c, m, y), model=CMODEL_CMY)
[docs]def font_weight_from_line_thickness(line_thick, height): """ Returns font weight for a text height and line thickness. :param line_thick: line thickness in same units as the text height :param height: text height :returns: one of: :: FONT_WEIGHT_ULTRALIGHT FONT_WEIGHT_LIGHT FONT_WEIGHT_MEDIUM FONT_WEIGHT_BOLD FONT_WEIGHT_XBOLD FONT_WEIGHT_XXBOLD .. versionadded:: 9.2 """ if height <= 0.: return FONT_WEIGHT_ULTRALIGHT ratio = line_thick / height fw = 1 for f in _weight_factor: if ratio <= f: return fw fw += 1 return FONT_WEIGHT_MEDIUM
[docs]def thickness_from_font_weight(weight, height): """ Returns the line thickness appropriate for a text weight. :param weight: one of: :: FONT_WEIGHT_ULTRALIGHT FONT_WEIGHT_LIGHT FONT_WEIGHT_MEDIUM FONT_WEIGHT_BOLD FONT_WEIGHT_XBOLD FONT_WEIGHT_XXBOLD :param height: font height .. versionadded:: 9.2 """ return height * _weight_factor[weight - 1]
[docs]class Text_def: """ Text definition: :param font: font name. TrueType fonts are assumed unless the name ends with '.gfn', which is a Geosoft gfn font. :param weight: one of: :: FONT_WEIGHT_ULTRALIGHT FONT_WEIGHT_LIGHT FONT_WEIGHT_MEDIUM FONT_WEIGHT_BOLD FONT_WEIGHT_XBOLD FONT_WEIGHT_XXBOLD :param line_thick: line thickness from which to determine a weight, which is calculated from the ratio of line thickness to height. :param italics: True for italics fonts :param height: text height, default 0.25 :param factor: default spatial properties are multiplied by this factor. This is useful for creating text scaled to the units of a view. The default text properties are scaled to cm. :Properties: :height: font height in view units :font: font name :weight: font weight, one of FONT_WEIGHT :line_thick: font line thickness for gfn stroke fonts :italics: True for italics :slant: Slant angle for stroke fonts, 0 if normal, 15 for italics :mapplot_string: mapplot compatible text definition string .. versionadded:: 9.2 """
[docs] def __init__(self, **kwargs): self._color = None self._font = None self._height = None self._gfn = None self._weight = None self._italics = None if 'default' in kwargs: def_pen = kwargs.pop('default') self.__dict__ = def_pen.__dict__.copy() else: self.color = Color(C_BLACK) self.height = 0.25 self.font = 'DEFAULT' self.gfn = True self.weight = None self.italics = False factor = kwargs.pop('factor', 1.) if factor != 1.0: self.height *= factor line_thick = None for k in kwargs: if k == 'color': self.color = kwargs[k] elif k == 'line_thick': line_thick = kwargs[k] elif k == 'font': self.font = kwargs[k] elif k in self.__dict__: self.__dict__[k] = kwargs[k] else: raise GroupException(_t('Invalid text definition parameter ({})'.format(k))) if self.weight is None: if line_thick is None: self.weight = FONT_WEIGHT_MEDIUM else: self.weight = font_weight_from_line_thickness(line_thick, self.height)
def __eq__(self, other): if hasattr(other, '__dict__'): return self.__dict__ == other.__dict__ return False def __ne__(self, other): return self.__dict__ != other.__dict__ @property def color(self): """text color as a :class:`Color` instance, can be set""" return self._color @color.setter def color(self, color): if isinstance(color, Color): self._color = color else: self._color = Color(color) @property def font(self): """text font name, can be set.""" return self._font @font.setter def font(self, font): if font: if '.gfn' in font.lower(): self.gfn = True self._font = font.lower().replace('.gfn', '') else: self.gfn = False self._font = font.replace('(TT)', '') else: self._font = 'DEFAULT' self.gfn = True @property def line_thick(self): """text line thickness determined from the font weight, can be set.""" return thickness_from_font_weight(self.weight, self.height) @line_thick.setter def line_thick(self, line_thick): self.weight = font_weight_from_line_thickness(line_thick, self.height) @property def slant(self): """text slant, 15 for italics, 0 for not italics, can be set. If set, any slant greater than 5 will result in a 15 degree slant to create italics.""" if self.italics: return 15 else: return 0 @slant.setter def slant(self, slant): if slant > 5: self.italics = True else: self.italics = False @property def mapplot_string(self): """ Mapplot text definition string, assumes scaling in cm. """ if 'default' in self._font.lower(): font = 'DEFAULT' elif not self.gfn: font = self._font.strip() + '(TT)' else: font = self._font return '{},,,{},"{}"'.format(self.height, self.slant, font)
[docs]class Pen: """ Geosoft Pen class. The default dimensioned properties (`line_thick`, `line_pitch`, `pat_size` and `pat_thick`) assume the view units are cm, and this is usually only the case for the base view. For views in other units either explicitly define the dimention in view units, or pass `factor` set the the view :attr:`geosoft.gxpy.view.View.units_per_map_cm`. :param line_color: line :class:`Color` instance, default is black :param fill_color: fill :class:`Color` instance, default is transparent :param line_thick: line thickness, default is 0.01 :param line_style: line pattern style :: LINE_STYLE_SOLID (default) LINE_STYLE_LONG LINE_STYLE_DOTTED LINE_STYLE_SHORT LINE_STYLE_LONG_SHORT_LONG LINE_STYLE_LONG_DOT_LONG :param line_pitch: line style pitch, default is 0.5 :param line_smooth: smooth line: :: SMOOTH_NONE (default) SMOOTH_AKIMA SMOOTH_CUBIC :param pat_number: pattern number for filled patterns (refer to `etc/default.pat`) default 0, flood fill :param pat_angle: pattern angle, default 0 :param pat_density: pattern density, default 1 :param pat_size: pattern size, default 1.0 :param pat_style: pattern style: :: TILE_RECTANGULAR (default) TILE_DIAGONAL TILE_TRIANGULAR TILE_RANDOM :param pat_thick: pattern line thickness, default 0.01 :param default: default :class:`Pen` instance, if specified defaults are established from this :param factor: default spatial properties are multiplied by this factor. This is useful for creating pens scaled to the units of a view. The default pen properties are scaled to cm. Typically you will pass :attr:`geosoft.gxpy.view.View.units_per_map_cm`. .. versionadded: 9.2 """
[docs] def __init__(self, **kwargs): self._line_color = None self._line_thick = None self._line_style = None self._line_pitch = None self._line_smooth = None self._fill_color = None self._pat_number = None self._pat_angle = None self._pat_density = None self._pat_size = None self._pat_style = None self.__pat_thick = None if 'default' in kwargs: def_pen = kwargs.pop('default') self.__dict__ = def_pen.__dict__.copy() else: self.line_color = Color(C_BLACK) self.line_thick = 0.01 self.line_style = LINE_STYLE_SOLID self.line_pitch = 0.5 self.line_smooth = SMOOTH_NONE self.fill_color = Color(C_TRANSPARENT) self.pat_number = 0 self.pat_angle = 0 self.pat_density = 1 self.pat_size = 1 self.pat_style = TILE_RECTANGULAR self.pat_thick = self.line_thick factor = kwargs.pop('factor', 1.) if factor != 1.0: self.line_thick *= factor self.line_pitch *= factor self.pat_size *= factor self.pat_thick *= factor for k in kwargs: if k == 'line_color': self.line_color = kwargs[k] elif k == 'fill_color': self.fill_color = kwargs[k] elif k in self.__dict__: self.__dict__[k] = kwargs[k] else: raise GroupException(_t('Invalid pen parameter ({})'.format(k)))
[docs] @classmethod def from_mapplot_string(cls, cstr): """ Create a :class:`Pen` instance from a mapplot-style string descriptor using either a krgbKRGB or kcmyKCMY color model. Lower case letters indicate line color, uppercase indicates fill color, 'k', 'K' for black. Each letter may be followed by an intensity between 0 and 255. If an intensity is not specified 255 is assumed. Line thickness can be defined by 't' followed by a thickness in 1000'th of the view unit, which for the default 'base' view would be microns. :param cstr: mapplot-style color definition Examples: =========== ============================================== 'r' red line 'R' red fill 'rG64' red line, light-green fill 'c64' light cyan line, equivalent to 'R191G255B255' 'c64K96' light cyan line, light-grey fill 'bt500' blue line, 0.5 units thick =========== ============================================== .. versionadded:: 9.2 """ def color_model(colstr): s = colstr.lower() for c in 'cmy': if c in s: return 'cmyk' return 'rgbk' def get_part(colstr, c, default=255): if c not in colstr: return 0 start = colstr.index(c) end = start + 1 for c in colstr[end:]: if not (c in '0123456789'): break end += 1 if end == start + 1: return default return int(colstr[start + 1:end]) def add_k(c, k): return max(c[0] - k, 0), max(c[1] - k, 0), max(c[2] - k, 0) def has_color(colstr, cc): for c in cc: if c in colstr: return True return False def color(colstr, cc): if has_color(colstr, cc): k = get_part(colstr, cc[3]) if has_color(colstr, cc[:3]): if model[0] == 'c' or model[0] == 'C': return add_k((255 - get_part(colstr, cc[0]), 255 - get_part(colstr, cc[1]), 255 - get_part(colstr, cc[2])), k) else: return add_k((get_part(colstr, cc[0]), get_part(colstr, cc[1]), get_part(colstr, cc[2])), k) else: return add_k((255, 255, 255), k) else: return C_TRANSPARENT model = color_model(cstr) line_color = color(cstr, model) fill_color = color(cstr, model.upper()) line_thick = max(1, get_part(cstr, 't', 1)) * 0.001 return cls(line_color=line_color, fill_color=fill_color, line_thick=line_thick)
def __eq__(self, other): for k, v in self.__dict__.items(): if other.__dict__[k] != v: return False return True @property def line_color(self): """pen line color as a :class:`color` instance, can be set.""" return self._line_color @line_color.setter def line_color(self, color): if isinstance(color, Color): self._line_color = color else: self._line_color = Color(color) @property def fill_color(self): return self._fill_color @fill_color.setter def fill_color(self, color): """pen fill color as a :class:`color` instance, can be set.""" if isinstance(color, Color): self._fill_color = color else: self._fill_color = Color(color) @property def mapplot_string(self): """line/fill colour and thickness string suing mapplor format, eg. 'kR125B64t1000'""" s = '' if self._line_color.int_value != C_TRANSPARENT: if self._line_color.int_value == C_BLACK: s += 'k' else: c = self._line_color.rgb s += 'r{}g{}b{}'.format(c[0], c[1], c[2]) if self._fill_color.int_value != C_TRANSPARENT: if self._line_color.int_value == C_BLACK: s += 'K' else: c = self._fill_color.rgb s += 'R{}G{}B{}'.format(c[0], c[1], c[2]) return s + 't{}'.format(int(self.line_thick * 1000.))
[docs]class Color_symbols_group(Group): """ Data represented as colored symbols based on a :class:`Color_map`. :Constructors: ============ ======================================= :func:`new` create a new symbol group in a view :func:`open` open an existing symbol group in a view ============ ======================================= """ def __exit__(self, exc_type, exc_val, exc_tb): self.__del__() def __del__(self): if hasattr(self, '_gxcsymb'): self._gxcsymb = None if hasattr(self, '_close'): self._close()
[docs] def __init__(self, view, group_name, **kwargs): self._gxcsymb = None super().__init__(view, group_name, **kwargs)
[docs] @classmethod def new(cls, view, name, data, color_map, symbol_def=None, symbol=SYMBOL_CIRCLE, mode=REPLACE, **kwargs): """ Create a new color symbols group with color mapping. If the group exists a new unique name is constructed. :param view: the view in which to place the group :param name: group name :param data: 2d numpy data array [[x, y, value], ...] or an iterable that yields `((x, y), value)`, or `((x, y, z), value, ...)`. Only the first `value` is used, an in the case of an iterable that yields (x, y, z) the z is ignored. :param color_map: symbol fill color :class:`Color_map`. Symbols are filled with the color lookup using `data`. :param symbol_def: :class:`Text_def` defines the symbol font to use, normally `symbols.gfn` is expected, and if used the symbols defined by the `SYMBOL` manifest are valid. For other fonts you will get the symbol requested. The default is `Text_def(font='symbols.gfn', color='k', weight=FONT_WEIGHT_ULTRALIGHT)` :param symbol: the symbol to plot, normally one of `SYMBOL`. :param mode: REPLACE (default) or NEW, which creates a new unique name if group exists :return: :class:`Color_symbols_group` instance .. versionadded:: 9.2 .. versionchanged:: 9.4 added support for passing data as a 2d numpy array """ def valid(xyd): if xyd[0][0] is None or xyd[0][1] is None or xyd[1] is None: return False return True cs = cls(view, name, mode=mode, **kwargs) cs._gxcsymb = gxapi.GXCSYMB.create(color_map.save_file()) if symbol_def is None: symbol_def = Text_def(font='geosoft.gfn', height=(0.25 * view.units_per_map_cm), weight=FONT_WEIGHT_ULTRALIGHT, color=C_BLACK) cs._gxcsymb.set_font(symbol_def.font, symbol_def.gfn, symbol_def.weight, symbol_def.italics) cs._gxcsymb.set_static_col(symbol_def.color.int_value, 0) cs._gxcsymb.set_scale(symbol_def.height) cs._gxcsymb.set_number(symbol) if isinstance(data, np.ndarray): if data.ndim != 2 or data.shape[1] < 3: raise GroupException(_t('data array must have shape (-1, 3)')) cs._gxcsymb.add_data(gxvv.GXvv(data[:, 0]).gxvv, gxvv.GXvv(data[:, 1]).gxvv, gxvv.GXvv(data[:, 2]).gxvv) else: xy = gxgm.PPoint([xy[0] for xy in data if valid(xy)]) cs._gxcsymb.add_data(gxvv.GXvv(xy.x).gxvv, gxvv.GXvv(xy.y).gxvv, gxvv.GXvv([d[1] for d in data if valid(d)]).gxvv) view.gxview.col_symbol(cs.name, cs._gxcsymb) if cs.unit_of_measure: color_map.unit_of_measure = cs.unit_of_measure return cs
[docs] @classmethod def open(cls, view, group_name): """ Open an existing color symbols group. :param view: view that contains the group :param group_name: name of the group, which must be a color symbols group :return: :class:`Color_symbols_group` instance .. versionadded:: 9.2 """ cs = cls(view, group_name, mode=READ_ONLY) group_number = view.gxview.find_group(group_name) cs._gxcsymb = view.gxview.get_col_symbol(group_number) return cs
[docs] def color_map(self): """ Return the :class:`geosoft.gxpy.group.Color_map` of a color symbol group. .. versionadded:: 9.3 """ itr = gxapi.GXITR.create() self._gxcsymb.get_itr(itr) cmap = geosoft.gxpy.group.Color_map(itr) cmap.title = self.name cmap.unit_of_measure = self.unit_of_measure return cmap
[docs]class Aggregate_group(Group): """ Aggregate group in a view :Constructors: ======== ================================ `open()` open an existing aggregate group `new()` create a new aggregate group ======== ================================ :Properties: :name: aggregate group name :agg: :class:`gxpy.agg.Aggregate_image` instance .. versionadded:: 9.2 """ def __exit__(self, exc_type, exc_val, exc_tb): self.__del__() def __del__(self): if hasattr(self, 'agg'): self.agg = None if hasattr(self, '_close'): self._close()
[docs] def __init__(self, view, group_name, mode): self.agg = None super().__init__(view, group_name, mode=mode)
[docs] @classmethod def new(cls, view, agg, name=None, mode=REPLACE, clip=True): """ Create a new aggregate group in a view. :param view: `geosoft.gxpy.view.View` or `geosoft.gxpy.view.View_3d` instance :param agg: `geosoft.gxpy.agg.Aggregate` instance. :param name: group name, default is the aggregate name :param mode: REPLACE (default) or NEW, which creates a unique name if the group exists :param clip: True to clip the agregare to the view clip limits .. versionadded:: 9.2 .. versionchanged:: 9.3.1 added clip mode """ if name is None: name = agg.name agg_group = cls(view, name, mode=mode) agg_group.agg = agg view.clip = clip view.gxview.aggregate(agg.gxagg, agg_group.name) view.clip = False return agg_group
[docs] @classmethod def open(cls, view, group_name): """ Open an existing aggregate group in a view. :param view: `geosoft.gxpy.view.View` or `geosoft.gxpy.view.View_3d` instance :param group_name: group name (or number) .. versionadded:: 9.2 """ agg_group = cls(view, group_name, mode=READ_ONLY) if isinstance(group_name, int): group_number = group_name else: group_number = view.gxview.find_group(agg_group.name) agg_group.agg = gxagg.Aggregate_image.open(view.gxview.get_aggregate(group_number)) return agg_group
[docs]class VoxDisplayGroup(Group): """ Vox display group in a view. Use class methods `new()` and `open()` to create instances of `VoxDisplayGroup`. :Constructors: ======== ================================== `open()` open an existing vox_display group `new()` create a new vox_display group ======== ================================== .. versionadded:: 9.3.1 """ def __exit__(self, exc_type, exc_val, exc_tb): self.__del__() def __del__(self): if hasattr(self, '_voxd'): self._voxd = None if hasattr(self, '_close'): self._close()
[docs] def __init__(self, view3d, group_name, mode=REPLACE): self._voxd = None if not view3d.is_3d: raise GroupException(_t('View must be 3d')) super().__init__(view3d, group_name, mode=mode)
[docs] @classmethod def new(cls, view3d, voxd, name=None, mode=REPLACE): """ Add a VoxDisplay as a new group in the view :param view3d: `geosoft.gxpy.view.View_3d` instance :param voxd: `geosoft.gxpy.vox_display.VoxDisplay` instance :param name: group name, default is the voxd name :param mode: REPLACE (default) or NEW, which creates a unique name if the group exists .. versionadded:: 9.3.1 """ if name is None: name = voxd.name voxd_group = cls(view3d, name, mode=mode) ext = voxd.vox.extent if voxd.is_vector: scale, height_base_ratio, max_base_size_ratio, max_cones = voxd.vector_cone_specs if max_cones is None: max_cones = gxapi.iDUMMY minimum_value = voxd.shell_limits[0] if minimum_value is None: minimum_value = 0. view3d.gxview.draw_vector_voxel_vectors(voxd.vox.gxvox, name, voxd.color_map.gxitr, scale, height_base_ratio, max_base_size_ratio, minimum_value, max_cones) # add to extent to make room for vectors cell2 = min(min(voxd.vox.cells_x), min(voxd.vox.cells_y), min(voxd.vox.cells_z)) * 4. ext = gxgm.Point2((ext.p0 - cell2, ext.p1 + cell2)) else: view3d.gxview.voxd(voxd.gxvoxd, voxd_group.name) view3d.add_extent(ext) voxd_group._voxd = voxd voxd_group.unit_of_measure = voxd.unit_of_measure return voxd_group
[docs] @classmethod def open(cls, view, group_name): """ Open an existing `VoxDisplayGroup` in a 3d view. :param view: the 3d view :param group_name: the name of the group to open, must be a `gxapi.GXVOXD` or `gxapi.GXVECTOR3D`. .. versionadded: 9.3.1 """ voxd_group = cls(view, group_name, mode=READ_ONLY) if view.gxview.is_group(group_name, gxapi.MVIEW_IS_VOXD): voxd_group._voxd = gxvoxd.VoxDisplay.gxapi_gxvoxd(voxd_group.view.gxview.get_voxd(voxd_group.number)) elif view.gxview.is_group(group_name, gxapi.MVIEW_IS_VECTOR3D): voxd_group._voxd = gxvoxd.VoxDisplay.gxapi_gxvoxd(voxd_group.view.gxview.get_vector_3d(voxd_group.number), name=group_name + ".geosoft_vectorvoxel") else: raise GroupException('Group "{}" is not a GXVOXD or a GXVECTOR3D'.format(group_name)) return voxd_group
@property def voxd(self): """ The `geosoft.gxpy.vox_display.VoxDisplay` for this vox group. .. versionadded:: 9.3.1 """ return self._voxd
[docs]class Color_map: """ Color map for establishing data color mapping for things like aggregates and color symbols. :param cmap: the name of a Geosoft color map file (`.tbl, .zon, .itr, .agg`) from which to establish the initial colors. If the file does not have zone values, which is the case for a `.tbl` file, the Color_map will be uninitialized and you can use one of the `set` methods to establish zone values. You can also provide an `int`, which will create an uninitialized map of the the specified length, or a :class:`geosoft.gxapi.GXITR` instance. If not specified the Geosoft default color table is used. :param title: Color map title which is displayed in the color map legend. :param unit_of_measure: Unit of measure to be displayed in a color map legend. .. versionadded:: 9.2 .. versionchanged:: 9.3 changed `units` to `unit_of_measure` for consistency across gxpy """
[docs] def __init__(self, cmap=None, title=None, unit_of_measure=None): if cmap is None: sr = gxapi.str_ref() if gxapi.GXSYS.global_('MONTAJ.DEFAULT_COLOUR', sr) == 0: cmap = sr.value if not cmap: cmap = 'colour' if isinstance(cmap, str): if cmap == 'color': cmap = 'colour' base, ext = os.path.splitext(cmap) if not ext: cmap = cmap + '.tbl' self.file_name = cmap self.gxitr = gxapi.GXITR.create_file(cmap) elif isinstance(cmap, int): self.gxitr = gxapi.GXITR.create() self.gxitr.set_size(cmap) for i in range(cmap): self.__setitem__(i, (gxapi.rMAX, C_BLACK)) self.file_name = None elif isinstance(cmap, gxapi.GXITR): self.gxitr = cmap else: raise ValueError('Cannot make a color map from: {}'.format(cmap)) self._next = 0 self._title = title self._units = unit_of_measure
def __iter__(self): return self def __next__(self): if self._next >= self.length: self._next = 0 raise StopIteration else: self._next += 1 return self.__getitem__(self._next - 1) def __getitem__(self, item): if item < 0 or item >= self.length: raise IndexError ir = gxapi.int_ref() self.gxitr.get_zone_color(item, ir) color = Color(ir.value) if item < self.length - 1: v = self.gxitr.get_zone_value(item) else: v = None return v, color def __setitem__(self, item, setting): if item < 0 or item >= self.length: raise IndexError if not isinstance(setting[1], int): setting = (setting[0], setting[1].int_value) self.gxitr.set_zone_color(item, setting[1]) if item < self.length - 1: self.gxitr.set_zone_value(item, setting[0]) def __eq__(self, other): if self.length != other.length: return False for i in range(self.length): if self[i] != other[i]: return False return True @property def title(self): """ Title, usually the name of the data from which the color bar was made or is intended. None if no title .. versionadded:: 9.2 """ return self._title @title.setter def title(self, title): if title: self._title = str(title) else: self._title = None @property def unit_of_measure(self): """ Data unit of measure for the data from which the color bar was made or is intended. None if the unit of measure is unknown. .. versionadded:: 9.2 """ return self._units @unit_of_measure.setter def unit_of_measure(self, units): if units: self._units = str(units) else: self._units = None @property def data_limits(self): """ Data limits of color map The limit values are for information only. Applications will assume that these values represent the largest and smallest values in a population represented by the ITR. If they are dummy, they have not been set. :returns: min/max tuple .. versionadded:: 9.4 """ min = gxapi.float_ref() max = gxapi.float_ref() self.gxitr.get_data_limits(min, max) return (min.value, max.value) @data_limits.setter def data_limits(self, limits): self.gxitr.get_data_limits(limits[0], limits[1]) @property def length(self): """ Number of color zones in the map. """ return self.gxitr.get_size() @property def brightness(self): """ Brightness is a value between -1 (black) and +1 (white), The default is 0. :returns: brightness, -1 to +1 .. versionadded:: 9.2 """ return self.gxitr.get_brightness() @property def color_map(self): """list of zone limts, colours in the color map""" return [vc for vc in self] @property def color_map_rgb(self): """list of zone limits and (red, green, blue) colours""" return [(vc[0], vc[1].rgb) for vc in self] @brightness.setter def brightness(self, value): """Map brightness between -1 (black ) and +1 (white. Can be set.""" self.gxitr.change_brightness(value) @property def model_type(self): """Geosoft colour model used in the Geosoft :class:`geosoft.gxapi.GXITR`""" return self.gxitr.get_zone_model_type() @property def initialized(self): """ Returns True if the color_map has been initialized to have zone boundaries. .. versionadded:: 9.2 """ return self.length > 0 and self[0][0] != gxapi.rMAX
[docs] def set_sequential(self, start=0, increment=1): """ Set color map zones based on a start and increment between each color zone. :param start: minimum zone boundary, values <= this value will have the first color :param increment: increment between each color. .. versionadded:: 9.2 """ if increment <= 0: raise ValueError(_t('increment must be > 0.')) for i in range(self.length - 1): self.gxitr.set_zone_value(i, start + i * increment)
[docs] def set_linear(self, minimum, maximum, inner_limits=True, contour_interval=None): """ Set the map boundaries based on a linear distribution between minimum and maximum. :param minimum: minimum :param maximum: maximum :param inner_limits: True if the range specifies the inner limits of the color mappings, in which case values less than or equal to the minimum are mapped to the first color and colors greater than the maximum are mapped to the last color. If False, the minimum and maximum are at the outer-edges of the color map. :param contour_interval: align color edges on this interval, which is useful for matching colors contour map, for example. The color map will be reduced in size by thinning of unneeded colors if necessary. .. versionadded:: 9.2 """ if inner_limits: if self.length < 3: raise GroupException(_t("Colour map must have length >= 3 for inner edge linear range.")) delta = (maximum - minimum) / (self.length - 2) minimum -= delta maximum += delta self.gxitr.linear(minimum, maximum, gxapi.rDUMMY if contour_interval is None else contour_interval)
[docs] def set_logarithmic(self, minimum, maximum, contour_interval=None): """ Set the color boundaries based on a logarithmic distribution between minimum and maximum. :param minimum: minimum, must be > 0 :param maximum: maximum :param contour_interval: align color edges on this interval, 10 for powers of 10. unneeded colors if necessary. .. versionadded:: 9.2 """ self.gxitr.log_linear(minimum, maximum, gxapi.rDUMMY if contour_interval is None else contour_interval)
[docs] def set_normal(self, standard_deviation, mean, expansion=1.0, contour_interval=None): """ Set the color boundaries using a normal distribution around a mean. :param standard_deviation: the standard deviation of the normal distribution. :param mean: maximum :param expansion: expand by this factor around the mean :param contour_interval: align color edges on this interval, 10 for powers of 10. unneeded colors if necessary. .. versionadded:: 9.2 """ self.gxitr.normal(standard_deviation, mean, expansion, gxapi.rDUMMY if contour_interval is None else contour_interval)
[docs] def color_of_value(self, value): """ Return the gxg.Color of a value. The mapping is determined with exclusive minima, inclusive maxima for each color level. Values <= level [0] are assigned the [0] color, and values greater than the the [n-2] level are assigned the [n-1] color. :param value: data value :returns: :class:`Color` instance .. versionadded:: 9.2 """ return Color(self.gxitr.color_value(value))
[docs] def save_file(self, file_name=None): """ Save to a Geosoft file, `.tbl`, `.itr` or `.zon`. If the file_name does not have an extension and the color_map has not been initialized a `.tbl` file is created (colors only), otherwise a `.itr` is created, which contains both zone boundaries and colors. :param file_name: file name, if None a temporary file is created This is useful for gxapi methods that require a colour map to be loaded from a file. Say you cave a Color_map instance named `cmap` and you want to create a GXCSYMB instance, which requires a colur map file: .. code:: cs = gxapi.GXCSYMB.create(cmap.save_file()) .. versionadded:: 9.2 """ if file_name is None: file_name = gx.gx().temp_file() fn, ext = os.path.splitext(file_name) if not ext: if self.initialized: file_name = fn + '.itr' else: file_name = fn + '.tbl' self.gxitr.save_file(file_name) return file_name