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})'
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
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
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})"
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
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)
class Example(HasTraits): color = UseEnum(Color, help="Color enum")
class Example2(HasTraits): color1 = UseEnum(Color, allow_none=False) color2 = UseEnum(Color)
class Example2(HasTraits): color = UseEnum(Color, allow_none=True)
class Example2(HasTraits): color = UseEnum(Color, default_value=Color.green)
class Example2(HasTraits): color1 = UseEnum(Color, default_value=None, allow_none=True) color2 = UseEnum(Color, allow_none=True)
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()
class Example2(HasTraits): color = UseEnum(Color)
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
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)
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
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()
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)
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)
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
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')
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