コード例 #1
0
ファイル: save_widget.py プロジェクト: pylbm/pylbm_ui
class SaveItem(Item):
    form_class = SaveForm
    update_text = 'Update save configuration'

    field_list = List()
    when = UseEnum(SaveType)
    when_properties = Unicode()

    def __init__(self, all_fields, **kwargs):
        super().__init__(all_fields, **kwargs)
        self.content.children = [f'{self}']

    def form2field(self):
        self.field_list = self.form.select_field.v_model
        self.when = SaveType(self.form.select_when.v_model)
        self.when_properties = self.form.when_properties.v_model

    def field2form(self):
        self.form.select_field.v_model = self.field_list
        self.form.select_when.v_model = self.when.value
        self.form.when_properties.v_model = self.when_properties

    def __str__(self):
        return ', '.join(
            self.field_list) + f' ({self.when.value}: {self.when_properties})'
コード例 #2
0
 def test_assign_bad_enum_value_number__raises_error(self):
     # -- CONVERT: number => Enum value (item)
     bad_numbers = [-1, 0, 5]
     for value in bad_numbers:
         self.assertIsInstance(value, int)
         assert UseEnum(Color).select_by_number(value, None) is None
         example = self.Example()
         with self.assertRaises(TraitError):
             example.color = value
コード例 #3
0
class ElasticCmd(HasTraits):

    mode = UseEnum(Mode, default_value=Mode.GLOBAL)

    index = Unicode("").tag(config=True)


    @observe('mode')
    def _mode_changed(self, change):
        if (self.index == "" and change['new'] == Mode.QUERY):
            raise CmdWarning("enter query mode without an index set")

    pass
コード例 #4
0
class Event(HasTraits):
    event_type = UseEnum(EventType)
    accepted = Bool(False)

    def __init__(self, event_type, *args, **kwargs):
        super().__init__(event_type=event_type, *args, **kwargs)

    def accept(self):
        self.accepted = True

    def ignore(self):
        self.accepted = False

    def __str__(self):
        return f"{self.__class__.__name__}(event_type={self.event_type})"
コード例 #5
0
class ImageSet(LockedXmlTraits, UrlContainer):
    """A set of images."""

    data_set_type = UseEnum(
        DataSetType,
        default_value=DataSetType.SKY).tag(xml=XmlSer.attr('DataSetType'))
    """The renderer mode to which these data apply.

    Possible values are ``"Earth"``, ``"Planet"``, ``"Sky"``, ``"Panorama"``,
    ``"SolarSystem"``, and ``"Sandbox"``.

    """
    reference_frame = Unicode('').tag(xml=XmlSer.attr('ReferenceFrame'))
    """TBD."""

    name = Unicode('').tag(xml=XmlSer.attr('Name'))
    """A name used to refer to this imageset.

    Various parts of the WWT internals reference imagesets by this name, so
    it should be distinctive.

    """
    url = Unicode('').tag(xml=XmlSer.attr('Url'))
    """The URL of the image data.

    Either a URL or a URL template. TODO: details

    """
    alt_url = Unicode('').tag(xml=XmlSer.attr('AltUrl'))
    """An alternative URL that provided the data.

    If provided and the Windows client attempts to load an imageset using this
    alternative URL, that imageset will be replaced by this one. This provides a
    mechanism for superseding old imagesets with improved versions.

    """
    dem_url = Unicode('').tag(xml=XmlSer.attr('DemUrl'))
    """The URL of the DEM data.

    Either a URL or a URL template. TODO: details

    """
    width_factor = Int(2).tag(xml=XmlSer.attr('WidthFactor'))
    """This is a legacy parameter. Leave it at 2."""

    base_tile_level = Int(0).tag(xml=XmlSer.attr('BaseTileLevel'))
    """The level of the highest (coarsest-resolution) tiling available.

    This should be zero except for special circumstances.

    """
    quad_tree_map = Unicode('').tag(xml=XmlSer.attr('QuadTreeMap'))
    """TBD."""

    tile_levels = Int(0).tag(xml=XmlSer.attr('TileLevels'))
    """The number of levels of tiling.

    Should be zero for untiled images. An image with ``tile_levels = 1`` has been
    broken into four tiles, each 256x256 pixels. For ``tile_levels = 2``, there are
    sixteen tiles, and the padded height of the tiled area is ``256 * 2**2 = 1024``
    pixels. Image with dimensions of 2048 pixels or smaller do not need to be tiled,
    so if this parameter is nonzero it will usually be 4 or larger.

    """
    base_degrees_per_tile = Float(0.0).tag(
        xml=XmlSer.attr('BaseDegreesPerTile'))
    """The angular scale of the image.

    For untiled images, should be the pixel scale: the number of degrees per
    pixel in the vertical direction. Non-square pixels are not supported.

    For tiled images, this is the height of the image with its dimensions
    padded out to the next largest power of 2 for tiling purposes. If a square
    image is 1200 pixels tall and has a height of 0.016 deg, the padded height
    would be 2048 pixels and this parameter should be set to
    0.016 * 2048 / 1200 = 0.0273.

    """
    file_type = Unicode('.png').tag(xml=XmlSer.attr('FileType'))
    """The extension of the image file(s) in this set, including a leading period.

    """
    bottoms_up = Bool(False).tag(xml=XmlSer.attr('BottomsUp'))
    """TBD."""

    projection = UseEnum(ProjectionType,
                         default_value=ProjectionType.SKY_IMAGE).tag(
                             xml=XmlSer.attr('Projection'))
    """The type of projection used to place this image on the sky.

    For untiled images, this should be "SkyImage". For tiled images, it should
    be "Tan". The :meth:`set_position_from_wcs` method will set this value
    appropriately based on :attr:`tile_levels`.

    """
    center_x = Float(0.0).tag(xml=XmlSer.attr('CenterX'))
    """The horizontal location of the center of the image’s projection coordinate
    system.

    For sky images, this is a right ascension in degrees.

    """
    center_y = Float(0.0).tag(xml=XmlSer.attr('CenterY'))
    """The vertical location of the center of the image’s projection coordinate
    system.

    For sky images, this is a declination in degrees.

    """
    offset_x = Float(0.0).tag(xml=XmlSer.attr('OffsetX'))
    """The horizontal positioning of the image relative to its projection
    coordinate system.

    For untiled sky images, the image is by default positioned such that its
    lower left lands at the center of the projection coordinate system (namely,
    ``center_x`` and ``center_y``). The offset is measured in pixels and moves
    the image leftwards. Therefore, ``offset_x = image_width / 2`` places the
    center of the image at ``center_x``. This parameter is therefore analogous
    to the WCS keyword ``CRVAL1``.

    For tiled sky images, the offset is measured in *degrees*, and a value of
    zero means that the *center* of the image lands at the center of the
    projection coordinate system.

    """
    offset_y = Float(0.0).tag(xml=XmlSer.attr('OffsetY'))
    """The vertical positioning of the image relative to its projection
    coordinate system.

    For untiled sky images, the image is by default positioned such that its
    lower left lands at the center of the projection coordinate system (namely,
    ``center_x`` and ``center_y``). The offset is measured in pixels and moves
    the image downwards. Therefore, ``offset_y = image_height / 2`` places the
    center of the image at ``center_y``. This parameter is therefore analogous
    to the WCS keyword ``CRVAL2``.

    For tiled sky images, the offset is measured in *degrees*, and a value of
    zero means that the *center* of the image lands at the center of the
    projection coordinate system.

    """
    rotation_deg = Float(0.0).tag(xml=XmlSer.attr('Rotation'))
    """The rotation of image’s projection coordinate system, in degrees.

    For sky images, this is East from North, i.e. counterclockwise.

    """
    band_pass = UseEnum(
        Bandpass,
        default_value=Bandpass.VISIBLE).tag(xml=XmlSer.attr('BandPass'))
    """The bandpass of the image data."""

    sparse = Bool(True).tag(xml=XmlSer.attr('Sparse'))
    """TBD."""

    elevation_model = Bool(False).tag(xml=XmlSer.attr('ElevationModel'))
    """TBD."""

    stock_set = Bool(False).tag(xml=XmlSer.attr('StockSet'))
    """TBD."""

    generic = Bool(False).tag(xml=XmlSer.attr('Generic'))
    """TBD."""

    mean_radius = Float(0.0).tag(xml=XmlSer.attr('MeanRadius'))
    """TBD."""

    credits = Unicode('').tag(xml=XmlSer.text_elem('Credits'))
    """Textual credits for the image originator."""

    credits_url = Unicode('').tag(xml=XmlSer.text_elem('CreditsUrl'))
    """A URL giving the source of the image or more information about its creation."""

    thumbnail_url = Unicode('').tag(xml=XmlSer.text_elem('ThumbnailUrl'))
    """A URL to a standard WWT thumbnail representation of this imageset."""

    description = Unicode('').tag(xml=XmlSer.text_elem('Description'))
    """
    A textual description of the imagery.

    This field is referenced a few times in the original WWT documentation, but
    is not actually implemented. The ``Place.description`` field is at least
    loaded from the XML.
    """

    msr_community_id = Int(0).tag(xml=XmlSer.attr('MSRCommunityId'))
    """The ID number of the WWT Community that this content came from."""

    msr_component_id = Int(0).tag(xml=XmlSer.attr('MSRComponentId'))
    """The ID number of this content item on the WWT Communities system."""

    permission = Int(0).tag(xml=XmlSer.attr('Permission'))
    "TBD."

    def _tag_name(self):
        return 'ImageSet'

    def mutate_urls(self, mutator):
        if self.url:
            self.url = mutator(self.url)

        if self.dem_url:
            self.dem_url = mutator(self.dem_url)

        if self.credits_url:
            self.credits_url = mutator(self.credits_url)

        if self.thumbnail_url:
            self.thumbnail_url = mutator(self.thumbnail_url)

    def set_position_from_wcs(self,
                              headers,
                              width,
                              height,
                              place=None,
                              fov_factor=1.7):
        """Set the positional information associated with this imageset to match a set
        of WCS headers.

        Parameters
        ----------
        headers : :class:`~astropy.io.fits.Header` or string-keyed dict-like
          A set of FITS-like headers including WCS keywords such as ``CRVAL1``.
        width : positive integer
          The width of the image associated with the WCS, in pixels.
        height : positive integer
          The height of the image associated with the WCS, in pixels.
        place : optional :class:`~wwt_data_formats.place.Place`
          If specified, the centering and zoom level of the :class:`~wwt_data_formats.place.Place`
          object will be set to match the center and size of this image.
        fov_factor : optional float
          If *place* is provided, its zoom level will be set so that the
          angular height of the client viewport is this factor times the
          angular height of the image. The default is 1.7.

        Returns
        -------
        self
          For convenience in chaining function calls.

        Notes
        -----
        Certain of the ImageSet parameters take on different meanings depending on
        whether the image in question is a tiled "study" or not. This method will alter
        its behavior depending on whether the :attr:`tile_levels` attribute is greater
        than zero. If you are computing coordinates for a tiled study, make sure to set
        this parameter *before* calling this function.

        For the time being, the WCS must be equatorial using the gnomonic
        (``TAN``) projection.

        Required keywords in *headers* are:

        - ``CTYPE1`` and ``CTYPE2``
        - ``CRVAL1`` and ``CRVAL2``
        - ``CRPIX1`` and ``CRPIX2``
        - Either:
          - ``CDELT1``, ``CDELT2``, ``PC1_1``, and ``PC1_2``; or
          - ``CD1_1``, ``CD2_2``

        If present ``PC1_2``, ``PC2_1``, ``CD1_2``, and/or ``CD2_1`` are used.
        If absent, they are assumed to be zero.

        """
        if headers['CTYPE1'] != 'RA---TAN' or headers['CTYPE2'] != 'DEC--TAN':
            raise ValueError(
                'WCS coordinates must be in an equatorial/TAN projection')

        # Figure out the stuff we need from the headers.

        ra_deg = headers['CRVAL1']
        dec_deg = headers['CRVAL2']
        crpix_x = headers['CRPIX1'] - 1
        crpix_y = headers['CRPIX2'] - 1

        if 'CD1_1' in headers:
            cd1_1 = headers['CD1_1']
            cd2_2 = headers['CD2_2']
            cd1_2 = headers.get('CD1_2', 0.0)
            cd2_1 = headers.get('CD2_1', 0.0)

            if cd1_1 * cd2_2 - cd1_2 * cd2_1 < 0:
                cd_sign = -1
            else:
                cd_sign = 1

            rot_rad = math.atan2(-cd_sign * cd1_2, cd2_2)
            scale_x = math.sqrt(cd1_1**2 + cd2_1**2) * cd_sign
            scale_y = math.sqrt(cd1_2**2 + cd2_2**2)
        else:
            scale_x = headers['CDELT1']
            scale_y = headers['CDELT2']
            pc1_1 = headers.get('PC1_1', 1.0)
            pc2_2 = headers.get('PC2_2', 1.0)
            pc1_2 = headers.get('PC1_2', 0.0)
            pc2_1 = headers.get('PC2_1', 0.0)

            det = pc1_1 * pc2_2 - pc1_2 * pc2_1

            if det < 0:
                pc_sign = -1
            else:
                pc_sign = 1

            rot_rad = math.atan2(-pc_sign * pc1_2, pc2_2)

            # I am not sure if this is "supposed" to be allowed, but I've seen it.
            rtdet = math.sqrt(pc_sign * det)
            scale_x *= rtdet
            scale_y *= rtdet

        # This is our best effort to make sure that the view centers on the
        # center of the image.

        try:
            from astropy.wcs import WCS
        except:
            center_ra_deg = ra_deg
            center_dec_deg = dec_deg
        else:
            wcs = WCS(headers)
            center = wcs.pixel_to_world(width / 2, height / 2)
            center_ra_deg = center.ra.deg
            center_dec_deg = center.dec.deg

        # Now, assign the fields

        self.data_set_type = DataSetType.SKY
        self.width_factor = 2
        self.center_x = ra_deg
        self.center_y = dec_deg
        self.rotation_deg = rot_rad * 180 / math.pi

        if self.tile_levels > 0:  # are we tiled?
            self.projection = ProjectionType.TAN
            self.offset_x = (width / 2 - crpix_x) * abs(scale_x)
            self.offset_y = (height / 2 - crpix_y) * scale_y
            self.base_degrees_per_tile = scale_y * 256 * 2**self.tile_levels
        else:
            self.projection = ProjectionType.SKY_IMAGE
            self.offset_x = crpix_x
            self.offset_y = crpix_y
            self.base_degrees_per_tile = scale_y

        if place is not None:
            place.data_set_type = DataSetType.SKY
            place.rotation_deg = 0.  # I think this is better than propagating the image rotation?
            place.ra_hr = center_ra_deg / 15.
            place.dec_deg = center_dec_deg
            # It is hardcoded that in sky mode, zoom_level = height of client FOV * 6.
            place.zoom_level = height * scale_y * fov_factor * 6

        return self

    def wcs_headers_from_position(self):
        """Compute a set of WCS headers for this ImageSet's positional information.

        Returns
        -------
        A string-keyed dict-like containing FITS/WCS header keywords such as
        ``CTYPE1``, ``CRPIX1``, etc.

        Notes
        -----
        At the moment, this function only works for ImageSets with a
        projection type of ``SKY_IMAGE``. Support for other projections
        *might* be added later, if the need arises..

        """
        rv = {
            'CTYPE1': 'RA---TAN',
            'CTYPE2': 'DEC--TAN',
            'CRVAL1': self.center_x,
            'CRVAL2': self.center_y,
        }

        if self.projection != ProjectionType.SKY_IMAGE:
            raise NotImplementError(
                'wcs_headers_from_position() only works if projection=SKY_IMAGE'
            )

        rv['CRPIX1'] = self.offset_x + 1
        rv['CRPIX2'] = self.offset_y + 1
        rv['CDELT2'] = self.base_degrees_per_tile  # = scale_y, above
        rv['CDELT1'] = -self.base_degrees_per_tile  # AFAICT, non-square pixels can't be expressed

        c = math.cos(self.rotation_deg * math.pi / 180)
        s = math.sin(self.rotation_deg * math.pi / 180)

        rv['PC1_1'] = c
        rv['PC1_2'] = -s
        rv['PC2_1'] = s
        rv['PC2_2'] = c

        return rv
コード例 #6
0
 class Example(HasTraits):
     enum1 = Enum(choices, allow_none=False)
     enum2 = CaselessStrEnum(choices, allow_none=False)
     enum3 = FuzzyEnum(choices, allow_none=False)
     enum4 = UseEnum(CSColor, allow_none=False)
コード例 #7
0
 class Example(HasTraits):
     color = UseEnum(Color, help="Color enum")
コード例 #8
0
 class Example2(HasTraits):
     color1 = UseEnum(Color, allow_none=False)
     color2 = UseEnum(Color)
コード例 #9
0
 class Example2(HasTraits):
     color = UseEnum(Color, allow_none=True)
コード例 #10
0
 class Example2(HasTraits):
     color = UseEnum(Color, default_value=Color.green)
コード例 #11
0
 class Example2(HasTraits):
     color1 = UseEnum(Color, default_value=None, allow_none=True)
     color2 = UseEnum(Color, allow_none=True)
コード例 #12
0
class LanguageServerSession(LoggingConfigurable):
    """ Manage a session for a connection to a language server
    """

    argv = List(
        trait=Unicode,
        default_value=[],
        help="the command line arguments to start the language server",
    )
    languages = List(
        trait=Unicode,
        default_value=[],
        help="the languages this session can provide language server features",
    )
    process = Instance(
        subprocess.Popen, help="the language server subprocess", allow_none=True
    )
    writer = Instance(stdio.LspStdIoWriter, help="the JSON-RPC writer", allow_none=True)
    reader = Instance(stdio.LspStdIoReader, help="the JSON-RPC reader", allow_none=True)
    from_lsp = Instance(
        Queue, help="a queue for string messages from the server", allow_none=True
    )
    to_lsp = Instance(
        Queue, help="a queue for string message to the server", allow_none=True
    )
    handlers = Set(
        trait=Instance(WebSocketHandler),
        default_value=[],
        help="the currently subscribed websockets",
    )
    status = UseEnum(SessionStatus, default_value=SessionStatus.NOT_STARTED)
    last_handler_message_at = Instance(datetime, allow_none=True)
    last_server_message_at = Instance(datetime, allow_none=True)

    _tasks = None

    def __init__(self, *args, **kwargs):
        """ set up the required traitlets and exit behavior for a session
        """
        super().__init__(*args, **kwargs)
        atexit.register(self.stop)

    def __repr__(self):  # pragma: no cover
        return "<LanguageServerSession(languages={}, argv={})>".format(
            self.languages, self.argv
        )

    def to_json(self):
        return dict(
            languages=self.languages,
            handler_count=len(self.handlers),
            status=self.status.value,
            last_server_message_at=self.last_server_message_at.isoformat()
            if self.last_server_message_at
            else None,
            last_handler_message_at=self.last_handler_message_at.isoformat()
            if self.last_handler_message_at
            else None,
        )

    def initialize(self):
        """ (re)initialize a language server session
        """
        self.stop()
        self.status = SessionStatus.STARTING
        self.init_queues()
        self.init_process()
        self.init_writer()
        self.init_reader()

        loop = asyncio.get_event_loop()
        self._tasks = [
            loop.create_task(coro())
            for coro in [self._read_lsp, self._write_lsp, self._broadcast_from_lsp]
        ]

        self.status = SessionStatus.STARTED

    def stop(self):
        """ clean up all of the state of the session
        """

        self.status = SessionStatus.STOPPING

        if self.process:
            self.process.terminate()
            self.process = None
        if self.reader:
            self.reader.close()
            self.reader = None
        if self.writer:
            self.writer.close()
            self.writer = None

        if self._tasks:
            [task.cancel() for task in self._tasks]

        self.status = SessionStatus.STOPPED

    @observe("handlers")
    def _on_handlers(self, change: Bunch):
        """ re-initialize if someone starts listening, or stop if nobody is
        """
        if change["new"] and not self.process:
            self.initialize()
        elif not change["new"] and self.process:
            self.stop()

    def write(self, message):
        """ wrapper around the write queue to keep it mostly internal
        """
        self.last_handler_message_at = self.now()
        self.to_lsp.put_nowait(message)

    def now(self):
        return datetime.now(timezone.utc)

    def init_process(self):
        """ start the language server subprocess
        """
        self.process = subprocess.Popen(
            self.argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE
        )

    def init_queues(self):
        """ create the queues
        """
        self.from_lsp = Queue()
        self.to_lsp = Queue()

    def init_reader(self):
        """ create the stdout reader (from the language server)
        """
        self.reader = stdio.LspStdIoReader(
            stream=self.process.stdout, queue=self.from_lsp, parent=self
        )

    def init_writer(self):
        """ create the stdin writer (to the language server)
        """
        self.writer = stdio.LspStdIoWriter(
            stream=self.process.stdin, queue=self.to_lsp, parent=self
        )

    async def _read_lsp(self):
        await self.reader.read()

    async def _write_lsp(self):
        await self.writer.write()

    async def _broadcast_from_lsp(self):
        """ loop for reading messages from the queue of messages from the language
            server
        """
        async for msg in self.from_lsp:
            self.last_server_message_at = self.now()
            for handler in self.handlers:
                handler.write_message(msg)
            self.from_lsp.task_done()
コード例 #13
0
 class Example2(HasTraits):
     color = UseEnum(Color)
コード例 #14
0
class Place(LockedXmlTraits, UrlContainer):
    """A place that can be visited."""

    data_set_type = UseEnum(
        DataSetType,
        default_value=DataSetType.EARTH).tag(xml=XmlSer.attr("DataSetType"))
    name = Unicode("").tag(xml=XmlSer.attr("Name"))
    ra_hr = Float(0.0).tag(xml=XmlSer.attr("RA"), xml_if_sky_type_is=True)
    dec_deg = Float(0.0).tag(xml=XmlSer.attr("Dec"), xml_if_sky_type_is=True)
    latitude = Float(0.0).tag(xml=XmlSer.attr("Lat"), xml_if_sky_type_is=False)
    longitude = Float(0.0).tag(xml=XmlSer.attr("Lng"),
                               xml_if_sky_type_is=False)
    constellation = UseEnum(Constellation,
                            default_value=Constellation.UNSPECIFIED).tag(
                                xml=XmlSer.attr("Constellation"))
    classification = UseEnum(Classification,
                             default_value=Classification.UNSPECIFIED).tag(
                                 xml=XmlSer.attr("Classification"))
    magnitude = Float(0.0).tag(xml=XmlSer.attr("Magnitude"))
    distance = Float(0.0).tag(xml=XmlSer.attr("Distance"), xml_omit_zero=True)
    angular_size = Float(0.0).tag(xml=XmlSer.attr("AngularSize"))
    zoom_level = Float(0.0).tag(xml=XmlSer.attr("ZoomLevel"))
    rotation_deg = Float(0.0).tag(xml=XmlSer.attr("Rotation"))
    angle = Float(0.0).tag(xml=XmlSer.attr("Angle"))
    opacity = Float(100.0).tag(xml=XmlSer.attr("Opacity"))
    dome_alt = Float(0.0).tag(xml=XmlSer.attr("DomeAlt"), xml_omit_zero=True)
    dome_az = Float(0.0).tag(xml=XmlSer.attr("DomeAz"), xml_omit_zero=True)
    background_image_set = Instance(
        ImageSet,
        allow_none=True).tag(xml=XmlSer.wrapped_inner("BackgroundImageSet"))
    foreground_image_set = Instance(
        ImageSet,
        allow_none=True).tag(xml=XmlSer.wrapped_inner("ForegroundImageSet"))
    image_set = Instance(ImageSet,
                         allow_none=True).tag(xml=XmlSer.inner("ImageSet"))
    thumbnail = Unicode("").tag(xml=XmlSer.attr("Thumbnail"))

    description = Unicode("").tag(xml=XmlSer.text_elem("Description"))
    """
    A description of the place, using HTML markup.

    This field is not actually used in the stock WWT clients, but it is wired up
    and loaded from the XML.
    """

    annotation = Unicode("").tag(xml=XmlSer.attr("Annotation"))
    """
    Annotation metadata for the place.

    This field is only used in the web engine and web client app. The web client
    app expects this field to contain a comma-separated list of key-value pairs,
    where each pair is delimited with colons:

    .. code-block::

        key1:val1,key2:val2,key3:val3

    The webclient includes some unfinished support for this field to be used to
    create circular annotations with YouTube video links. If your WTML file will
    not be viewed in the webclient, you can use this field to convey arbitrary
    textual data to the WWT Web Engine JavaScript/TypeScript layer.

    """

    msr_community_id = Int(0).tag(xml=XmlSer.attr("MSRCommunityId"),
                                  xml_omit_zero=True)
    """The ID number of the WWT Community that this content came from."""

    msr_component_id = Int(0).tag(xml=XmlSer.attr("MSRComponentId"),
                                  xml_omit_zero=True)
    """The ID number of this content item on the WWT Communities system."""

    permission = Int(0).tag(xml=XmlSer.attr("Permission"), xml_omit_zero=True)
    "TBD."

    xmeta = Instance(
        Namespace,
        args=(),
        help=
        "XML metadata - a namespace object for attaching arbitrary text to serialize",
    ).tag(xml=XmlSer.ns_to_attr("X"))

    def _tag_name(self):
        return "Place"

    def mutate_urls(self, mutator):
        if self.thumbnail:
            self.thumbnail = mutator(self.thumbnail)

        if self.background_image_set:
            self.background_image_set.mutate_urls(mutator)

        if self.foreground_image_set:
            self.foreground_image_set.mutate_urls(mutator)

        if self.image_set:
            self.image_set.mutate_urls(mutator)

    def as_imageset(self):
        """Return an ImageSet for this place if one is defined.

        Returns
        -------
        Either :class:`wwt_data_formats.imageset.ImageSet` or None.

        Notes
        -----
        If the :attr:`foreground_image_set` of this :class:`Place` is not
        None, it is returned. Otherwise, if its :attr:`image_set` is not
        None, that is returned. Otherwise, None is returned.

        """
        if self.foreground_image_set is not None:
            return self.foreground_image_set
        return self.image_set
コード例 #15
0
class Folder(LockedXmlTraits, UrlContainer):
    """A grouping of WWT content assets.

    Children can be: places (aka "Items"), imagesets, linesets, tours,
    folders, or IThumbnail objects (to be explored).

    """

    name = Unicode("").tag(xml=XmlSer.attr("Name"))
    group = Unicode("Explorer").tag(xml=XmlSer.attr("Group"))
    url = Unicode("").tag(xml=XmlSer.attr("Url"))
    """The URL at which the full contents of this folder can be downloaded in WTML
    format.

    """
    thumbnail = Unicode("").tag(xml=XmlSer.attr("Thumbnail"))
    browseable = Bool(True).tag(xml=XmlSer.attr("Browseable"))
    searchable = Bool(True).tag(xml=XmlSer.attr("Searchable"))
    type = UseEnum(
        FolderType,
        default_value=FolderType.UNSPECIFIED,
    ).tag(xml=XmlSer.attr("Type"))
    sub_type = Unicode("").tag(xml=XmlSer.attr("SubType"))
    msr_community_id = Int(0).tag(xml=XmlSer.attr("MSRCommunityId"),
                                  xml_omit_zero=True)
    """The ID number of the WWT Community that this content came from."""

    msr_component_id = Int(0).tag(xml=XmlSer.attr("MSRComponentId"),
                                  xml_omit_zero=True)
    """The ID number of this content item on the WWT Communities system."""

    permission = Int(0).tag(xml=XmlSer.attr("Permission"), xml_omit_zero=True)
    "TBD."

    children = List(
        trait=Union([
            Instance("wwt_data_formats.folder.Folder", args=()),
            Instance("wwt_data_formats.place.Place", args=()),
            Instance("wwt_data_formats.imageset.ImageSet", args=()),
        ]),
        default_value=(),
    ).tag(xml=XmlSer.inner_list())

    def _tag_name(self):
        return "Folder"

    def walk(self, download=False):
        yield (0, (), self)

        for index, child in enumerate(self.children):
            if isinstance(child, Folder):
                if not len(child.children) and child.url and download:
                    url = child.url
                    child = Folder.from_url(url)
                    child.url = url
                    self.children[index] = child

                for depth, path, subchild in child.walk(download=download):
                    yield (depth + 1, (index, ) + path, subchild)
            else:
                yield (1, (index, ), child)

    def mutate_urls(self, mutator):
        if self.url:
            self.url = mutator(self.url)
        if self.thumbnail:
            self.thumbnail = mutator(self.thumbnail)

        for c in self.children:
            c.mutate_urls(mutator)

    def immediate_imagesets(self):
        """
        Generate a sequence of the imagesets defined in this folder, without
        recursion into any child folders.

        Returns
        -------
        A generator of tuples of ``(child_index, item_type, imageset)``, described below.

        Notes
        -----
        In the generated tuples, ``child_index`` is the index number of the item
        within the folder's :attr:`~Folder.children` array and ``imageset`` is
        the :class:`~wwt_data_formats.imageset.ImageSet` object contained within
        the folder. If ``item_type`` is ``None``, that indicates that the
        imageset corresponds to an imageset child that is defined directly in
        the folder contents. It may also be a string indicating that the
        imageset is defined by a different kind of potential folder child.
        Allowed values are ``"place_imageset"``, ``"place_foreground"``, or
        ``"place_background"``, for different imagesets that may be contained
        within a :class:`~wwt_data_formats.place.Place` item in the folder.

        Examples
        --------
        Consider a folder that has two children: an imageset, and a place. The
        place in turn defines both a
        :attr:`~wwt_data_formats.place.Place.foreground_image_set` and a
        :attr:`~wwt_data_formats.place.Place.background_image_set`. The
        generator returned by this function will yield three values: ``(0, None,
        <ImageSet>)``, ``(1, "place_foreground", <ImageSet>)``, and ``(1,
        "place_background", <ImageSet>)``.
        """

        from .imageset import ImageSet
        from .place import Place

        for index, child in enumerate(self.children):
            if isinstance(child, ImageSet):
                yield (index, None, child)
            elif isinstance(child, Place):
                if child.image_set is not None:
                    yield (index, "place_imageset", child.image_set)
                if child.foreground_image_set is not None:
                    yield (index, "place_foreground",
                           child.foreground_image_set)
                if child.background_image_set is not None:
                    yield (index, "place_background",
                           child.background_image_set)
コード例 #16
0
class JSSHeaderPreprocessor(Preprocessor):
    """ Adds a bunch of nasty CSS and JavaScript to make fonts pretty.
    """

    static_strategy = UseEnum(
        StaticStrategy,
        default_value=StaticStrategy.inline_js,
        config=True,
        help=
        "A strategy for applying style from JSS.. only `inline_js` implemented",
    )

    def normalize_jss(self, nb, resources):
        jss = nb.metadata.get(METADATA_KEY)
        if jss is None:
            return

        try:
            if not jss["styles"].get(":root"):
                jss["styles"].pop(":root", None)
        except Exception:
            return False

        for key in list(jss.keys()):
            if not jss[key]:
                del jss[key]

        return jss

    def preprocess(self, nb, resources):
        msg_tmpl = " Adding %s bytes of fonts, style, and scripts%s"

        jss = self.normalize_jss(nb, resources)

        # any falsey value
        if not jss:
            self.log.info(msg_tmpl, 0, " (skipping nbjss)")
            return nb, resources
        inlining = resources.setdefault("inlining", {})
        inlining.setdefault("css", "")

        css = None

        if self.static_strategy == StaticStrategy.inline_js:
            css = self.strategy_inline_js(jss, resources)
        else:
            raise NotImplementedError(
                "Sorry, {} is not yet supported for static_strategy".format(
                    self.static_strategy))

        if css is not None:
            self.log.info(msg_tmpl, len(css), " (nbjss).")
            inlining["css"] += [css]

        return nb, resources

    @property
    def nbjss_jss_json(self):
        with open(nbjss_jss) as fp:
            return json.load(fp)

    @property
    def nbjss_js(self):
        with open(nbjss_js) as fp:
            return fp.read()

    def strategy_inline_js(self, jss, resources):
        """ Embed
        """
        css = """</style>
        <script>%s</script>
        <script>
            ;(function(){
                nbjss.createStyleSheet(%s).attach();
                nbjss.createStyleSheet(%s).attach();
            }).call(this);
        </script>
        <style>/* this is not style */""" % (
            self.nbjss_js,
            json.dumps({"@global": self.nbjss_jss_json}),
            json.dumps(
                {
                    "@global": jss.get("styles", {}),
                    "@font-face": sum(jss.get("fonts", {}).values(), []),
                },
                indent=2,
            ),
        )
        return css
コード例 #17
0
class LanguageServerSession(LoggingConfigurable):
    """ Manage a session for a connection to a language server
    """

    language_server = Unicode(help="the language server implementation name")
    spec = Schema(LANGUAGE_SERVER_SPEC)

    # run-time specifics
    process = Instance(subprocess.Popen,
                       help="the language server subprocess",
                       allow_none=True)
    writer = Instance(stdio.LspStdIoWriter,
                      help="the JSON-RPC writer",
                      allow_none=True)
    reader = Instance(stdio.LspStdIoReader,
                      help="the JSON-RPC reader",
                      allow_none=True)
    from_lsp = Instance(Queue,
                        help="a queue for string messages from the server",
                        allow_none=True)
    to_lsp = Instance(Queue,
                      help="a queue for string message to the server",
                      allow_none=True)
    handlers = Set(
        trait=Instance(WebSocketHandler),
        default_value=[],
        help="the currently subscribed websockets",
    )
    status = UseEnum(SessionStatus, default_value=SessionStatus.NOT_STARTED)
    last_handler_message_at = Instance(datetime, allow_none=True)
    last_server_message_at = Instance(datetime, allow_none=True)

    _tasks = None

    _skip_serialize = ["argv", "debug_argv"]

    def __init__(self, *args, **kwargs):
        """ set up the required traitlets and exit behavior for a session
        """
        super().__init__(*args, **kwargs)
        atexit.register(self.stop)

    def __repr__(self):  # pragma: no cover
        return ("<LanguageServerSession("
                "language_server={language_server}, argv={argv})>").format(
                    language_server=self.language_server, **self.spec)

    def to_json(self):
        return dict(
            handler_count=len(self.handlers),
            status=self.status.value,
            last_server_message_at=self.last_server_message_at.isoformat()
            if self.last_server_message_at else None,
            last_handler_message_at=self.last_handler_message_at.isoformat()
            if self.last_handler_message_at else None,
            spec={
                k: v
                for k, v in self.spec.items() if k not in SKIP_JSON_SPEC
            },
        )

    def initialize(self):
        """ (re)initialize a language server session
        """
        self.stop()
        self.status = SessionStatus.STARTING
        self.init_queues()
        self.init_process()
        self.init_writer()
        self.init_reader()

        loop = asyncio.get_event_loop()
        self._tasks = [
            loop.create_task(coro()) for coro in
            [self._read_lsp, self._write_lsp, self._broadcast_from_lsp]
        ]

        self.status = SessionStatus.STARTED

    def stop(self):
        """ clean up all of the state of the session
        """

        self.status = SessionStatus.STOPPING

        if self.process:
            self.process.terminate()
            self.process = None
        if self.reader:
            self.reader.close()
            self.reader = None
        if self.writer:
            self.writer.close()
            self.writer = None

        if self._tasks:
            [task.cancel() for task in self._tasks]

        self.status = SessionStatus.STOPPED

    @observe("handlers")
    def _on_handlers(self, change: Bunch):
        """ re-initialize if someone starts listening, or stop if nobody is
        """
        if change["new"] and not self.process:
            self.initialize()
        elif not change["new"] and self.process:
            self.stop()

    def write(self, message):
        """ wrapper around the write queue to keep it mostly internal
        """
        self.last_handler_message_at = self.now()
        IOLoop.current().add_callback(self.to_lsp.put_nowait, message)

    def now(self):
        return datetime.now(timezone.utc)

    def init_process(self):
        """ start the language server subprocess
        """
        self.process = subprocess.Popen(
            self.spec["argv"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            env=self.substitute_env(self.spec.get("env", {}), os.environ),
        )

    def init_queues(self):
        """ create the queues
        """
        self.from_lsp = Queue()
        self.to_lsp = Queue()

    def init_reader(self):
        """ create the stdout reader (from the language server)
        """
        self.reader = stdio.LspStdIoReader(stream=self.process.stdout,
                                           queue=self.from_lsp,
                                           parent=self)

    def init_writer(self):
        """ create the stdin writer (to the language server)
        """
        self.writer = stdio.LspStdIoWriter(stream=self.process.stdin,
                                           queue=self.to_lsp,
                                           parent=self)

    def substitute_env(self, env, base):
        final_env = copy(os.environ)

        for key, value in env.items():
            final_env.update(
                {key: string.Template(value).safe_substitute(base)})

        return final_env

    async def _read_lsp(self):
        await self.reader.read()

    async def _write_lsp(self):
        await self.writer.write()

    async def _broadcast_from_lsp(self):
        """ loop for reading messages from the queue of messages from the language
            server
        """
        async for message in self.from_lsp:
            self.last_server_message_at = self.now()
            await self.parent.on_server_message(message, self)
            self.from_lsp.task_done()
コード例 #18
0
class Folder(LockedXmlTraits, UrlContainer):
    """A grouping of WWT content assets.

    Children can be: places (aka "Items"), imagesets, linesets, tours,
    folders, or IThumbnail objects (to be explored).

    """
    name = Unicode('').tag(xml=XmlSer.attr('Name'))
    group = Unicode('Explorer').tag(xml=XmlSer.attr('Group'))
    url = Unicode('').tag(xml=XmlSer.attr('Url'))
    """The URL at which the full contents of this folder can be downloaded in WTML
    format.

    """
    thumbnail = Unicode('').tag(xml=XmlSer.attr('Thumbnail'))
    browseable = Bool(True).tag(xml=XmlSer.attr('Browseable'))
    searchable = Bool(True).tag(xml=XmlSer.attr('Searchable'))
    type = UseEnum(
        FolderType,
        default_value = FolderType.SKY,
    ).tag(xml=XmlSer.attr('Type'))
    sub_type = Unicode('').tag(xml=XmlSer.attr('SubType'))
    msr_community_id = Int(0).tag(xml=XmlSer.attr('MSRCommunityId'))
    """The ID number of the WWT Community that this content came from."""

    msr_component_id = Int(0).tag(xml=XmlSer.attr('MSRComponentId'))
    """The ID number of this content item on the WWT Communities system."""

    permission = Int(0).tag(xml=XmlSer.attr('Permission'))
    "TBD."

    children = List(
        trait = Union([
            Instance('wwt_data_formats.folder.Folder', args=()),
            Instance('wwt_data_formats.place.Place', args=()),
            Instance('wwt_data_formats.imageset.ImageSet', args=()),
        ]),
        default_value = ()
    ).tag(xml=XmlSer.inner_list())

    def _tag_name(self):
        return 'Folder'

    def walk(self, download=False):
        yield (0, (), self)

        for index, child in enumerate(self.children):
            if isinstance(child, Folder):
                if not len(child.children) and child.url and download:
                    url = child.url
                    child = Folder.from_url(url)
                    child.url = url
                    self.children[index] = child

                for depth, path, subchild in child.walk(download=download):
                    yield (depth + 1, (index,) + path, subchild)
            else:
                yield (1, (index,), child)

    def mutate_urls(self, mutator):
        if self.url:
            self.url = mutator(self.url)
        if self.thumbnail:
            self.thumbnail = mutator(self.thumbnail)

        for c in self.children:
            c.mutate_urls(mutator)
コード例 #19
0
ファイル: traitutils.py プロジェクト: bopopescu/scipyen
def gen_trait_from_type(x, *args, **kwargs):
    """Generates a TraitType for object x.
    
    Prerequisites: Except for enum types (enum.Enum and enumIntEnum)
    x.__class__ should define a "copy constructor", e.g.:
    
    x = SomeClass()
    
    y = SomeClass(x)
    
    For types derived from builtin types, this is taken care of by the python 
    library. Anything else needs 
    
    """
    immclass = getmro(x.__class__)[0]
    # NOTE 2020-07-07 14:42:22
    # to prevent "slicing" of derived classes, 
    
    arg = [x] + [a for a in args]
    
    args = tuple(arg)
    
    kw = kwargs
    
    if isinstance(x, bool):
        if immclass != bool:
            # preserve its immediate :class:, otherwise this will slice subclasses
            return Instance(klass = x.__class__, args=args, kw=kw)
        
        return Bool(default_value=x)
    
    elif isinstance(x, int):
        if immclass != int:
            return Instance(klass = x.__class__, args=args, kw=kw)
        
        return Int(default_value=x)
    
    elif isinstance(x, float):
        if immclass != float:
            return Instance(klass = x.__class__, args=args, kw=kw)
        
        return Float(default_value=x)
    
    elif isinstance(x, complex):
        if immclass != complex:
            return Instance(klass = x.__class__, args=args, kw=kw)
        
        return Complex(default_value=x)
    
    elif isinstance(x, bytes):
        if immclass != bytes:
            return Instance(klass = x.__class__, args=args, kw=kw)
        
        return Bytes(default_value=x)
    
    elif isinstance(x, str):
        if immclass != str:
            return Instance(klass = x.__class__, args=args, kw=kw)
        
        return Unicode(default_value=x)
    
    elif isinstance(x, list):
        if immclass != list:
            return Instance(klass = x.__class__, args=args, kw=kw)
        
        return List(default_value=x)
    
    elif isinstance(x, set):
        if immclass != set:
            return Instance(klass = x.__class__, args=args, kw=kw)
        
        return Set(default_value = x)
    
    elif isinstance(x, tuple):
        if immclass != tuple:
            return Instance(klass = x.__class__, args=args, kw=kw)
        
        return Tuple(default_value=x)
    
    elif isinstance(x, dict):
        if immclass != dict:
            # preserve its immediate :class:, otherwise this will slice subclasses
            return Instance(klass = x.__class__, args=args, kw=kw)
        
        return Dict(default_value=x)

    
    elif isinstance(x, enum.EnumMeta):
        return UseEnum(x)
    
    else:
        #immclass = getmro(x.__class__)[0]
        if immclass.__name__ == "type":
            return Type(klass=x, default_value = immclass)
        
        else:
            return Instance(klass = x.__class__, args=args, kw=kw)
コード例 #20
0
class ImageSet(LockedXmlTraits, UrlContainer):
    """
    A WWT imagery dataset.

    Instances of this class express WWT imagery datasets and their spatial
    positioning. Imagesets are exposed to the WWT engine through their XML
    serialization in a WTML :class:`~wwt_data_formats.folder.Folder`. The engine
    can be instructed to load such a folder, making its imagesets available for
    rendering.
    """

    data_set_type = UseEnum(
        DataSetType,
        default_value=DataSetType.SKY).tag(xml=XmlSer.attr("DataSetType"))
    """The renderer mode to which these data apply.

    Possible values are ``"Earth"``, ``"Planet"``, ``"Sky"``, ``"Panorama"``,
    ``"SolarSystem"``, and ``"Sandbox"``.

    """
    reference_frame = Unicode("").tag(xml=XmlSer.attr("ReferenceFrame"))
    """TBD."""

    name = Unicode("").tag(xml=XmlSer.attr("Name"))
    """A name used to refer to this imageset.

    Various parts of the WWT internals reference imagesets by this name, so
    it should be distinctive.

    """
    url = Unicode("").tag(xml=XmlSer.attr("Url"))
    """The URL of the image data.

    Either a URL or a URL template. URLs that are exposed to the engine should
    be absolute and use the ``http://`` protocol (the web engine will rewrite
    them to HTTPS if needed). The ``wwtdatatool`` program that comes with this
    package provides some helpful utilities to allow data-processing to use relative URLs. TODO: more
    details.

    """
    alt_url = Unicode("").tag(xml=XmlSer.attr("AltUrl"))
    """An alternative URL that provided the data.

    If provided and the Windows client attempts to load an imageset using this
    alternative URL, that imageset will be replaced by this one. This provides a
    mechanism for superseding old imagesets with improved versions.

    """
    dem_url = Unicode("").tag(xml=XmlSer.attr("DemUrl"))
    """The URL of the DEM data.

    Either a URL or a URL template. TODO: details

    """
    width_factor = Int(2).tag(xml=XmlSer.attr("WidthFactor"))
    """This is a legacy parameter. Leave it at 2."""

    base_tile_level = Int(0).tag(xml=XmlSer.attr("BaseTileLevel"))
    """The level of the highest (coarsest-resolution) tiling available.

    This should be zero except for special circumstances.

    """
    quad_tree_map = Unicode("").tag(xml=XmlSer.attr("QuadTreeMap"))
    """TBD."""

    tile_levels = Int(0).tag(xml=XmlSer.attr("TileLevels"))
    """The number of levels of tiling.

    Should be zero for untiled images (``projection =
    ProjectionType.SkyImage``).

    For tiled images (``projection = ProjectionType.Tan``), an image with
    ``tile_levels = 1`` has been broken into four tiles, each 256x256 pixels.
    For ``tile_levels = 2``, there are sixteen tiles, and the padded height of
    the tiled area is ``256 * 2**2 = 1024`` pixels. Image with dimensions of
    2048 pixels or smaller do not need to be tiled, so if this parameter is
    nonzero it will usually be 4 or larger.

    """
    base_degrees_per_tile = Float(0.0).tag(
        xml=XmlSer.attr("BaseDegreesPerTile"))
    """The angular scale of the image.

    For untiled images, this is the pixel scale: the number of degrees per pixel
    in the vertical direction. Non-square pixels are not supported.

    For tiled images, this is the angular height of the image, in degrees, after
    its dimensions have been padded out to the next largest power of 2 for
    tiling purposes. If a square image is 1200 pixels tall and has a height of
    0.016 deg, the padded height would be 2048 pixels and this parameter should
    be set to 0.016 * 2048 / 1200 = 0.0273.

    """
    file_type = Unicode(".png").tag(xml=XmlSer.attr("FileType"))
    """
    The extension(s) of the image file(s) in this set.

    In the simplest case, this field will contain an image filetype extension
    including the leading period, such as ``.jpeg`` or ``.png``. Some datasets
    in the wild lack the leading period: they have just ``png`` or something
    similar. The value ``.auto`` is also used in some cases, which can be OK
    because often WWT doesn't actually use this field for any particular
    purpose.

    Some datasets, like HiPS imagery, provide multiple filetypes simultaneously.
    These can be expressed by including several filename extensions separated by
    spaces. For instance, ``png jpeg fits``. The existing WTML records that
    support multiple filetypes do not include any leading periods, but clients
    should be prepared for them to be present.

    Imagesets to be rendered as FITS data *must* have the exact value ``.fits``
    for this field. If multiple filetypes are specified, the special
    FITS-rendering machinery will not be invoked. This is true for both single
    FITS files and tiled FITS imagesets, including HiPS FITS datasets.

    A supported filetype extension of ``tsv`` (or ``.tsv``) means that this
    "imageset" actually contains a HiPS progressive catalog, not bitmap imagery.
    Imageset records should not intermix image-type and catalog-type filetypes.
    (We don't know if there are any examples in the wild of HiPS datasets that
    claim to contain both kinds of data.)
    """

    bottoms_up = Bool(False).tag(xml=XmlSer.attr("BottomsUp"))
    """
    The parity of the image's projection on the sky.

    For untiled (``projection = SkyImage``) images, this flag defines the
    image's parity, which basically sets whether the image needs to be flipped
    during rendering. This field should be False for typical RGB color images
    that map onto the sky as if you had taken them with a digital camera. For
    these images, the first row of image data is at the top of the image at zero
    rotation. For typical FITS files, on the other hand, the first row of image
    data is at the bottom of the image, which results in a parity inversion. In
    these cases, the ``bottoms_up`` flag should be True (hence its name). In the
    terminology of `Astrometry.Net
    <https://astroquery.readthedocs.io/en/latest/astrometry_net/astrometry_net.html#parity>`_,
    ``bottoms_up = False`` corresponds to negative parity, and ``bottoms_up =
    True`` corresponds to positive parity.

    The effect of setting this flag to True is to effectively flip the image and
    its coordinate system left-to-right. For a ``bottoms_up = False`` image with
    :attr:`offset_x`, :attr:`offset_y`, and :attr:`rotation_deg` all zero, the
    lower-left corner of the image lands at the :attr:`center_x` and
    :attr:`center_y`, and positive rotations rotate the image counter-clockwise
    around that origin. If you take the same image and make ``bottoms_up =
    True``, the image will appear to have been flipped left-to-right, the
    lower-*right* corner of the image will land at the coordinate center, and
    positive rotations will rotate it *clockwise* around that origin. In both
    cases, positive values of :attr:`offset_x` and :attr:`offset_y` move the
    center of the image closer to the coordinate center, but when ``bottoms_up =
    False``, this means that the image is moving down and left, and when
    ``bottoms_up = True`` this means that the image is moving down and right.

    For tiled images (``projection = Tan``), this field must be false. If it is
    true, the imageset won't render.
    """

    projection = UseEnum(ProjectionType,
                         default_value=ProjectionType.SKY_IMAGE).tag(
                             xml=XmlSer.attr("Projection"))
    """The type of projection used to place this image on the sky.

    For untiled images, this should be "SkyImage". For tiled images, it should
    be "Tan". The :meth:`set_position_from_wcs` method will set this value
    appropriately based on :attr:`tile_levels`.

    """
    center_x = Float(0.0).tag(xml=XmlSer.attr("CenterX"))
    """The horizontal location of the center of the image’s projection
    coordinate system.

    For sky images, this is a right ascension in degrees. Note that this
    parameter just helps to define a coordinate system; it does not control how
    the actual image data are placed onto that coordinate system. The
    :attr:`offset_x` and :attr:`offset_y` parameters do that.

    """
    center_y = Float(0.0).tag(xml=XmlSer.attr("CenterY"))
    """The vertical location of the center of the image’s projection coordinate
    system.

    For sky images, this is a declination in degrees. Note that this parameter
    just helps to define a coordinate system; it does not control how the actual
    image data are placed onto that coordinate system. The :attr:`offset_x` and
    :attr:`offset_y` parameters do that.

    """
    offset_x = Float(0.0).tag(xml=XmlSer.attr("OffsetX"), xml_omit_zero=True)
    """
    The horizontal positioning of the image relative to its projection
    coordinate system.

    For untiled sky images with :attr:`bottoms_up` false, the image is by
    default positioned such that its lower left corner lands at the center of
    the projection coordinate system (namely, :attr:`center_x` and
    :attr:`center_y`). The offset is measured in pixels and moves the image
    leftwards. Therefore, ``offset_x = image_width / 2`` places the horizontal
    center of the image at ``center_x``. This parameter is therefore analogous
    to the WCS keyword ``CRVAL1``.

    For untiled sky images where :attr:`bottoms_up` is true, the X coordinate
    system has been mirrored. Therefore when this field is zero, the lower
    *right* corner of the image will land at the center of the projection
    coordinate system, and positive values will move the image to the right.

    For tiled sky images, the offset is measured in *degrees*, and a value of
    zero means that the *center* of the image lands at the center of the
    projection coordinate system. Increasingly positive values move the image to
    the right.

    As per the usual practice, offsets are always along the horizontal axis of
    the image in question, regardless of its :attr:`rotation <rotation_deg>` on
    the sky.

    """
    offset_y = Float(0.0).tag(xml=XmlSer.attr("OffsetY"), xml_omit_zero=True)
    """The vertical positioning of the image relative to its projection
    coordinate system.

    For untiled sky images with :attr:`bottoms_up` false, the image is by
    default positioned such that its lower left corner lands at the center of
    the projection coordinate system (namely, :attr:`center_x` and
    :attr:`center_y`). The offset is measured in pixels and moves the image
    downwards. Therefore, ``offset_y = image_height / 2`` places the vertical
    center of the image at ``center_y``. This parameter is therefore analogous
    to the WCS keyword ``CRVAL2``.

    For untiled sky images where :attr:`bottoms_up` is true, the X coordinate
    system has been mirrored but the Y coordinate system is the same. Therefore
    when this field is zero, the lower *right* corner of the image will land at
    the center of the projection coordinate system, but positive values will
    still move the image downwards.

    For tiled sky images, the offset is measured in *degrees*, and a value of
    zero means that the *center* of the image lands at the center of the
    projection coordinate system. Increasingly positive values move the image
    upwards.

    As per the usual practice, offsets are always along the vertical axis of the
    image in question, regardless of its :attr:`rotation <rotation_deg>` on the
    sky.

    """
    rotation_deg = Float(0.0).tag(xml=XmlSer.attr("Rotation"))
    """
    The rotation of image’s projection coordinate system, in degrees.

    For sky images with :attr:`bottoms_up` false, this is East from North, i.e.
    counterclockwise. If :attr:`bottoms_up` is true (only allowed for untiled
    images), the image coordinate system is mirrored, and positive rotations
    rotate the image *clockwise* relative to the sky.
    """

    band_pass = UseEnum(
        Bandpass,
        default_value=Bandpass.VISIBLE).tag(xml=XmlSer.attr("BandPass"))
    """The bandpass of the image data."""

    sparse = Bool(True).tag(xml=XmlSer.attr("Sparse"))
    """TBD."""

    elevation_model = Bool(False).tag(xml=XmlSer.attr("ElevationModel"))
    """TBD."""

    stock_set = Bool(False).tag(xml=XmlSer.attr("StockSet"))
    """TBD."""

    generic = Bool(False).tag(xml=XmlSer.attr("Generic"))
    """TBD."""

    mean_radius = Float(0.0).tag(xml=XmlSer.attr("MeanRadius"),
                                 xml_omit_zero=True)
    """TBD."""

    credits = Unicode("").tag(xml=XmlSer.text_elem("Credits"))
    """Textual credits for the image originator."""

    credits_url = Unicode("").tag(xml=XmlSer.text_elem("CreditsUrl"))
    """A URL giving the source of the image or more information about its creation."""

    thumbnail_url = Unicode("").tag(xml=XmlSer.text_elem("ThumbnailUrl"))
    """A URL to a standard WWT thumbnail representation of this imageset."""

    description = Unicode("").tag(xml=XmlSer.text_elem("Description"))
    """
    A textual description of the imagery.

    This field is referenced a few times in the original WWT documentation, but
    is not actually implemented. The ``Place.description`` field is at least
    loaded from the XML.
    """

    msr_community_id = Int(0).tag(xml=XmlSer.attr("MSRCommunityId"),
                                  xml_omit_zero=True)
    """The ID number of the WWT Community that this content came from."""

    msr_component_id = Int(0).tag(xml=XmlSer.attr("MSRComponentId"),
                                  xml_omit_zero=True)
    """The ID number of this content item on the WWT Communities system."""

    permission = Int(0).tag(xml=XmlSer.attr("Permission"), xml_omit_zero=True)
    "TBD."

    pixel_cut_low = Float(0.0).tag(xml=XmlSer.attr("PixelCutLow"),
                                   xml_omit_zero=True)
    """Suggested default low cutoff value when displaying FITS."""

    pixel_cut_high = Float(0.0).tag(xml=XmlSer.attr("PixelCutHigh"),
                                    xml_omit_zero=True)
    """Suggested default high cutoff value when displaying FITS."""

    data_min = Float(0.0).tag(xml=XmlSer.attr("DataMin"), xml_omit_zero=True)
    """Lowest data value of a FITS file."""

    data_max = Float(0.0).tag(xml=XmlSer.attr("DataMax"), xml_omit_zero=True)
    """Highest data value of a FITS file."""
    def _tag_name(self):
        return "ImageSet"

    def mutate_urls(self, mutator):
        if self.url:
            self.url = mutator(self.url)

        if self.dem_url:
            self.dem_url = mutator(self.dem_url)

        if self.credits_url:
            self.credits_url = mutator(self.credits_url)

        if self.thumbnail_url:
            self.thumbnail_url = mutator(self.thumbnail_url)

    def set_position_from_wcs(self,
                              headers,
                              width,
                              height,
                              place=None,
                              fov_factor=1.7):
        """
        Set the positional information associated with this imageset to match a
        set of WCS headers.

        Parameters
        ----------
        headers : :class:`~astropy.io.fits.Header` or string-keyed dict-like

            A set of FITS-like headers including WCS keywords such as ``CRVAL1``.

        width : positive integer

            The width of the image associated with the WCS, in pixels.

        height : positive integer

            The height of the image associated with the WCS, in pixels.

        place : optional :class:`~wwt_data_formats.place.Place`

            If specified, the centering and zoom level of the
            :class:`~wwt_data_formats.place.Place` object will be set to match
            the center and size of this image.

        fov_factor : optional

            If *place* is provided, its zoom level will be set so that the angular
            height of the client viewport is this factor times the angular height of
            the image. The default is 1.7.

        Returns
        -------
        **self**

            For convenience in chaining function calls.

        Notes
        -----
        Certain of the ImageSet parameters take on different meanings depending
        on whether the image in question is a tiled "study" or not. This method
        will alter its behavior depending on whether the :attr:`tile_levels`
        attribute is greater than zero. **Make sure that this parameter has
        acquired its final value before calling this function.**

        For the time being, the WCS must be equatorial using the gnomonic
        (``TAN``) projection.

        Required keywords in *headers* are:

        - ``CTYPE1`` and ``CTYPE2``
        - ``CRVAL1`` and ``CRVAL2``
        - ``CRPIX1`` and ``CRPIX2``
        - Either:
            - ``CD1_1``, ``CD2_2`` (preferred) or
            - ``CDELT1``, ``CDELT2``, ``PC1_1``, and ``PC1_2``;

        If present ``PC1_2``, ``PC2_1``, ``CD1_2``, and/or ``CD2_1`` are used.
        If absent, they are assumed to be zero.

        This routine assumes that the WCS coordinates have the correct parity
        for the data that they describe. If these WCS coordinates describe a
        JPEG-like image, the parity of the coordinates should be negative
        ("top-down"), which means that the determinant of the CD matrix should
        have a *positive* sign. If these coordinates describe a FITS-like image,
        the parity should be positive or "bottoms-up", with a negative
        determinant of the CD matrix. If the image in question is tiled, the
        parity must be top-down, in the sense the bottoms-up tiled imagery just
        won't render in the engine. There are some CD matrices that can't be
        expressed in WWT's formalism (rotation, scale, parity) and this method
        will do its best to detect and reject them.
        """

        if headers["CTYPE1"] != "RA---TAN" or headers["CTYPE2"] != "DEC--TAN":
            raise ValueError(
                "WCS coordinates must be in an equatorial/TAN projection")

        # Figure out the stuff we need from the headers.

        ra_deg = headers["CRVAL1"]
        dec_deg = headers["CRVAL2"]

        # In FITS/WCS, pixel coordinates are 1-based and integer pixel
        # coordinates land on pixel centers. Therefore in standard FITS
        # orientation, where the "first" pixel is at the lower-left, the
        # lower-left corner of the image has pixel coordinate (0.5, 0.5). For
        # the WWT offset parameters, the lower-left corner of the image has
        # coordinate (0, 0).
        refpix_x = headers["CRPIX1"] - 0.5
        refpix_y = headers["CRPIX2"] - 0.5

        if "CD1_1" in headers:
            cd1_1 = headers["CD1_1"]
            cd2_2 = headers["CD2_2"]
            cd1_2 = headers.get("CD1_2", 0.0)
            cd2_1 = headers.get("CD2_1", 0.0)
        else:
            # older PC/CDELT form -- note that we're using two additional
            # numbers to express the same information.
            d1 = headers["CDELT1"]
            d2 = headers["CDELT2"]
            cd1_1 = d1 * headers.get("PC1_1", 1.0)
            cd2_2 = d2 * headers.get("PC2_2", 1.0)
            cd1_2 = d1 * headers.get("PC1_2", 0.0)
            cd2_1 = d2 * headers.get("PC2_1", 0.0)

        cd_det = cd1_1 * cd2_2 - cd1_2 * cd2_1

        if cd_det < 0:
            cd_sign = -1

            if self.tile_levels > 0:
                raise Exception(
                    "WCS for tiled imagery must have top-down/negative/JPEG_like parity"
                )
        else:
            cd_sign = 1

        # Given how WWT implements its rotation coordinates, this expression
        # turns out to give us the correct value for different rotations and
        # parities:

        rot_rad = math.atan2(-cd_sign * cd1_2, -cd2_2)

        # We can only express square pixels with a rotation and a potential
        # parity flip. Do some cross-checks to ensure that the input matrix can
        # be well-approximated this way. There's probably a smarter
        # linear-algebra way to do this.

        TOL = 0.05

        if not abs(cd_det) > 0:
            raise ValueError("determinant of the CD matrix is not positive")

        scale_x = math.sqrt(cd1_1**2 + cd1_2**2)
        scale_y = math.sqrt(cd2_1**2 + cd2_2**2)

        if abs(scale_x - scale_y) / (scale_x + scale_y) > TOL:
            raise ValueError(
                "WWT cannot express non-square pixels, which this WCS has")

        det_scale = math.sqrt(abs(cd_det))

        if abs((cd1_1 - cd_sign * cd2_2) / det_scale) > TOL:
            raise ValueError(
                f"WWT cannot express this CD matrix (1; {cd1_1} {cd_sign} {cd2_2} {det_scale})"
            )

        if abs((cd2_1 + cd_sign * cd1_2) / det_scale) > TOL:
            raise ValueError(
                f"WWT cannot express this CD matrix (2; {cd2_1} {cd_sign} {cd1_2} {det_scale})"
            )

        # This is our best effort to make sure that the view centers on the
        # center of the image.

        try:
            from astropy.wcs import WCS
        except:
            center_ra_deg = ra_deg
            center_dec_deg = dec_deg
        else:
            wcs = WCS(headers)
            # The WCS object uses 0-based indices where the integer coordinates
            # land on pixel centers. Therefore the dead center of the image has
            # pixel coordinates as below. For instance, in an image 2 pixels
            # wide, the horizontal center is where the two pixels touch, which
            # has an X coordinate of 0.5.
            center = wcs.pixel_to_world((width - 1) / 2, (height - 1) / 2)
            center_ra_deg = center.ra.deg
            center_dec_deg = center.dec.deg

        # Now, assign the fields

        self.data_set_type = DataSetType.SKY
        self.width_factor = 2
        self.center_x = ra_deg
        self.center_y = dec_deg
        self.rotation_deg = rot_rad * 180 / math.pi

        if self.tile_levels > 0:  # are we tiled?
            self.projection = ProjectionType.TAN
            self.bottoms_up = False
            self.offset_x = (width / 2 - refpix_x) * scale_x
            self.offset_y = (refpix_y - height / 2) * scale_y
            self.base_degrees_per_tile = scale_y * 256 * 2**self.tile_levels
        else:
            self.projection = ProjectionType.SKY_IMAGE
            self.bottoms_up = cd_sign == -1
            self.offset_x = refpix_x
            self.offset_y = height - refpix_y
            self.base_degrees_per_tile = scale_y

            if self.bottoms_up:
                self.rotation_deg = -self.rotation_deg

        if place is not None:
            place.data_set_type = DataSetType.SKY
            place.rotation_deg = (
                0.0  # I think this is better than propagating the image rotation?
            )
            place.ra_hr = center_ra_deg / 15.0
            place.dec_deg = center_dec_deg
            # It is hardcoded that in sky mode, zoom_level = height of client FOV * 6.
            place.zoom_level = height * scale_y * fov_factor * 6

        return self

    def wcs_headers_from_position(self, height=None):
        """
        Compute a set of WCS headers for this ImageSet's positional information.

        Parameters
        ----------
        height : optional int
            The height of the underlying image, in pixels. This quantity is
            needed to compute WCS headers correctly for untiled (``SKY_IMAGE``
            projection) images. If this quantity is needed but not provided,
            a ValueError will be raised. Note that the :class:`ImageSet` class
            does *not* store this quantity.

        Returns
        -------
        A string-keyed dict-like containing FITS/WCS header keywords such as
        ``CTYPE1``, ``CRPIX1``, etc.

        Notes
        -----
        At the moment, this function only works for ImageSets with a projection
        type of ``SKY_IMAGE``. Support for other projections *might* be added
        later, if the need arises. Note, however, that tiled images have their
        sizes adjusted to be powers of 2, and the "actual" size of the source
        imagery is not necessarily preserved.
        """

        rv = {
            "CTYPE1": "RA---TAN",
            "CTYPE2": "DEC--TAN",
            "CRVAL1": self.center_x,
            "CRVAL2": self.center_y,
        }

        if self.projection != ProjectionType.SKY_IMAGE:
            raise NotImplementedError(
                "wcs_headers_from_position() only works if projection=SKY_IMAGE"
            )

        if height is None:
            raise ValueError(
                "must provide `height` to compute WCS headers for untiled images"
            )

        rv["CRPIX1"] = self.offset_x + 0.5
        rv["CRPIX2"] = height - self.offset_y + 0.5

        # The WWT rotation angle is 180 degrees away from the usual angle
        # that you would use for a rotation matrix, which is why we negate
        # these trig values:
        parity = -1 if self.bottoms_up else 1
        c = -math.cos(parity * self.rotation_deg * math.pi / 180)
        s = -math.sin(parity * self.rotation_deg * math.pi / 180)

        # | CD1_1 CD1_2 | = scale * | p 0 | * |  cos(theta) sin(theta) |
        # | CD2_1 CD2_2 |           | 0 1 |   | -sin(theta) cos(theta) |

        rv["CD1_1"] = c * self.base_degrees_per_tile * parity
        rv["CD1_2"] = s * self.base_degrees_per_tile * parity
        rv["CD2_1"] = -s * self.base_degrees_per_tile
        rv["CD2_2"] = c * self.base_degrees_per_tile

        return rv
コード例 #21
0
class LsystemWidget(PGLWidget):
    """TODO: Add docstring here
    """
    _model_name = Unicode('LsystemWidgetModel').tag(sync=True)
    _model_module = Unicode(module_name).tag(sync=True)
    _model_module_version = Unicode(module_version).tag(sync=True)
    _view_name = Unicode('LsystemWidgetView').tag(sync=True)
    _view_module = Unicode(module_name).tag(sync=True)
    _view_module_version = Unicode(module_version).tag(sync=True)

    __trees = []
    __filename = None
    __do_abort = False

    units = Unit
    derivationLength = Int(0, min=0).tag(sync=True)
    unit = UseEnum(Unit,
                   default_value=Unit.none).tag(sync=True,
                                                to_json=lambda e, o: e.value)
    scene = Dict(
        traits={
            'data': Bytes(),
            'scene': Instance(pgl.Scene),
            'derivationStep': Int(0, min=0),
            'id': Int(0, min=0)
        }).tag(sync=True, to_json=scene_to_json)
    animate = Bool(False).tag(sync=True)
    dump = Unicode('').tag(sync=False)
    is_magic = Bool(False).tag(sync=True)
    progress = Float(0.0).tag(sync=True)

    __editor = None
    __lp = None
    __codes = []
    __derivationStep = 0
    __in_queue = 0
    __out_queue = 0
    __lsystem = None
    __extra_context = {}

    def __init__(self,
                 filename=None,
                 code=None,
                 unit=Unit.none,
                 animate=False,
                 dump='',
                 context={},
                 lp=None,
                 **kwargs):

        if filename:
            if filename.endswith('.lpy'):
                self.__filename = filename
            else:
                self.__filename = str(filename) + '.lpy'

        if not self.__filename and not code:
            raise ValueError('Neither lpy file nor code provided')

        self.__lsystem = lpy.Lsystem()
        self.__extra_context = context
        self.__lp = lp
        code_ = ''
        if self.__filename and Path(self.__filename).is_file():
            with io.open(self.__filename, 'r') as file:
                code_ = file.read()
        else:
            self.is_magic = True
            code_ = code

        self.__codes = code_.split(lpy.LpyParsing.InitialisationBeginTag)
        self.__codes.insert(1, f'\n{lpy.LpyParsing.InitialisationBeginTag}\n')

        self.unit = unit
        self.animate = animate
        self.dump = dump
        self.on_msg(self.__on_custom_msg)

        if not isinstance(self.__lp, LsystemParameters):
            self.__initialize_parameters()
        self.__initialize_lsystem()
        self.__set_scene(0)

        super().__init__(**kwargs)

    @property
    def editor(self):
        if not self.is_magic:
            self.__editor = ParameterEditor(
                self.__lp, filename=self.__filename.split('.lpy')[0] + '.json')
            self.__editor.on_lpy_context_change = self.set_parameters
            return self.__editor
        else:
            return None

    def __initialize_parameters(self):
        self.__lp = LsystemParameters()
        if not self.is_magic:
            json_filename = self.__filename.split('.lpy')[0] + '.json'
            if Path(json_filename).exists():
                with io.open(json_filename, 'r') as file:
                    self.__lp.load(file)
            else:
                self.__lp.retrieve_from(lpy.Lsystem(self.__filename))
            self.__lp.filename = json_filename

    def get_lstring(self):
        if self.__derivationStep < len(self.__trees):
            return lpy.Lstring(self.__trees[self.__derivationStep])
        return lpy.Lstring()

    def get_namespace(self):
        result = {}
        result.update(self.__lsystem.context().globals())
        return result

    def set_parameters(self, parameters):
        """Update Lsystem parameters in context.

        Parameters
        ----------
        parameters : json
            A valid parameter json string.
        """

        if not self.animate:
            lp = LsystemParameters()
            try:
                lp.loads(parameters)
            except AssertionError:
                return False
            self.__in_queue = self.__in_queue + 1
            self.progress = self.__out_queue / self.__in_queue
            self.send_state('progress')

            self.__lsystem.clear()
            self.__lsystem.set(
                ''.join([self.__codes[0],
                         lp.generate_py_code()]), self.__extra_context)
            self.derivationLength = self.__lsystem.derivationLength + 1
            self.__trees = []
            self.__trees.append(self.__lsystem.axiom)
            self.__derivationStep = self.__derivationStep if self.__derivationStep < self.derivationLength else self.derivationLength - 1
            self.__derive(self.__derivationStep)

            self.__out_queue = self.__out_queue + 1
            self.progress = self.__out_queue / self.__in_queue
            if self.progress == 1:
                self.__out_queue = self.__in_queue = 0

            return True

        return False

    def __initialize_lsystem(self):
        self.__lsystem.clear()
        self.__lsystem.filename = self.__filename if self.__filename else ''
        self.__lsystem.set(
            ''.join([self.__codes[0],
                     self.__lp.generate_py_code()]), self.__extra_context)
        self.__derivationStep = 0
        self.derivationLength = self.__lsystem.derivationLength + 1
        self.__trees = [self.__lsystem.axiom]

    def __on_custom_msg(self, widget, content, buffers):
        if 'derive' in content:
            step = content['derive']
            if step < self.derivationLength:
                if step < len(self.__trees):
                    self.__set_scene(step)
                else:
                    self.__derive(step)
        elif 'rewind' in content:
            self.__rewind()

    def __rewind(self):
        self.__initialize_lsystem()
        self.__set_scene(0)

    def __set_scene(self, step):
        # print('__set_scene', step, self.__trees)
        scene = self.__lsystem.sceneInterpretation(self.__trees[step])
        serialized = serialize_scene(
            scene
        )  # bytes(scene_to_draco(scene, True).data) if self.compress else scene_to_bytes(scene)
        serialized_scene = {
            'data': serialized,
            'scene': scene,
            'derivationStep': step,
            'id': step
        }
        self.__derivationStep = step
        self.scene = serialized_scene
        if len(self.dump) > 0:
            os.makedirs(self.dump, exist_ok=True)
            file_type = 'bgeom'
            with io.open(
                    os.path.join(self.dump,
                                 f'{self.__filename}_{step}.{file_type}'),
                    'wb') as file:
                file.write(zlib.decompress(serialized))

    def __derive(self, step):
        if step < self.derivationLength:
            while True:
                # print('__derive', step)
                if step == len(self.__trees) - 1:
                    self.__set_scene(step)
                    break
                else:
                    self.__trees.append(
                        self.__lsystem.derive(self.__trees[-1],
                                              len(self.__trees), 1))
        else:
            raise ValueError(f'derivation step {step} out of Lsystem bounds')
コード例 #22
0
class Place(LockedXmlTraits, UrlContainer):
    """A place that can be visited."""

    data_set_type = UseEnum(
        DataSetType,
        default_value=DataSetType.EARTH).tag(xml=XmlSer.attr('DataSetType'))
    name = Unicode('').tag(xml=XmlSer.attr('Name'))
    ra_hr = Float(0.0).tag(xml=XmlSer.attr('RA'))
    dec_deg = Float(0.0).tag(xml=XmlSer.attr('Dec'))
    latitude = Float(0.0).tag(xml=XmlSer.attr('Lat'))
    longitude = Float(0.0).tag(xml=XmlSer.attr('Lng'))
    constellation = UseEnum(Constellation,
                            default_value=Constellation.UNSPECIFIED).tag(
                                xml=XmlSer.attr('Constellation'))
    classification = UseEnum(Classification,
                             default_value=Classification.UNSPECIFIED).tag(
                                 xml=XmlSer.attr('Classification'))
    magnitude = Float(0.0).tag(xml=XmlSer.attr('Magnitude'))
    distance = Float(0.0).tag(xml=XmlSer.attr('Distance'))
    angular_size = Float(0.0).tag(xml=XmlSer.attr('AngularSize'))
    zoom_level = Float(0.0).tag(xml=XmlSer.attr('ZoomLevel'))
    rotation_deg = Float(0.0).tag(xml=XmlSer.attr('Rotation'))
    angle = Float(0.0).tag(xml=XmlSer.attr('Angle'))
    opacity = Float(100.0).tag(xml=XmlSer.attr('Opacity'))
    dome_alt = Float(0.0).tag(xml=XmlSer.attr('DomeAlt'))
    dome_az = Float(0.0).tag(xml=XmlSer.attr('DomeAz'))
    background_image_set = Instance(
        ImageSet,
        allow_none=True).tag(xml=XmlSer.wrapped_inner('BackgroundImageSet'))
    foreground_image_set = Instance(
        ImageSet,
        allow_none=True).tag(xml=XmlSer.wrapped_inner('ForegroundImageSet'))
    image_set = Instance(ImageSet,
                         allow_none=True).tag(xml=XmlSer.inner('ImageSet'))
    thumbnail = Unicode('').tag(xml=XmlSer.attr('Thumbnail'))
    msr_community_id = Int(0).tag(xml=XmlSer.attr('MSRCommunityId'))
    """The ID number of the WWT Community that this content came from."""

    msr_component_id = Int(0).tag(xml=XmlSer.attr('MSRComponentId'))
    """The ID number of this content item on the WWT Communities system."""

    permission = Int(0).tag(xml=XmlSer.attr('Permission'))
    "TBD."

    xmeta = Instance(
        Namespace,
        args=(),
        help=
        'XML metadata - a namespace object for attaching arbitrary text to serialize',
    ).tag(xml=XmlSer.ns_to_attr('X'))

    def _tag_name(self):
        return 'Place'

    def mutate_urls(self, mutator):
        if self.thumbnail:
            self.thumbnail = mutator(self.thumbnail)

        if self.background_image_set:
            self.background_image_set.mutate_urls(mutator)

        if self.foreground_image_set:
            self.foreground_image_set.mutate_urls(mutator)

        if self.image_set:
            self.image_set.mutate_urls(mutator)

    def as_imageset(self):
        """Return an ImageSet for this place if one is defined.

        Returns
        -------
        Either :class:`wwt_data_formats.imageset.ImageSet` or None.

        Notes
        -----
        If the :attr:`foreground_image_set` of this :class:`Place` is not
        None, it is returned. Otherwise, if its :attr:`image_set` is not
        None, that is returned. Otherwise, None is returned.

        """
        if self.foreground_image_set is not None:
            return self.foreground_image_set
        return self.image_set