Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Footprints: custom regions import #2377

Merged
merged 18 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ Imviz
- vmin/vmax step size in the plot options plugin is now dynamic based on the full range of the
image. [#2388]

- Footprints plugin for plotting overlays of instrument footprints in the image viewer. [#2341]
- Footprints plugin for plotting overlays of instrument footprints or custom regions in the image
viewer. [#2341, #2377]

Mosviz
^^^^^^
Expand Down
6 changes: 5 additions & 1 deletion docs/imviz/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,18 @@ Footprints

This plugin supports loading and overplotting instrument footprint overlays on the image viewers.
Any number of overlays can be plotted simultaneously from any number of the available
preset instruments.
preset instruments (requires pysiaf to be installed) or by loading an Astropy regions object from
a file.

The top dropdown allows renaming, adding, and removing footprint overlays. To modify the display
and input parameters for a given overlay, select it in the dropdown, and modify the choices
in the plugin to change its color, opacity, visibilities in any image viewer in the app, or to
select between various preset instruments and change the input options (position on the sky,
position angle, offsets, etc).

To import a file, choose "From File..." from the presets dropdown and select a valid file (must
be able to be parsed by `regions.Regions.read`).



.. _rotate-canvas:
Expand Down
1 change: 1 addition & 0 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def to_unit(self, data, cid, values, original_units, target_units):
'plugin-editable-select': 'components/plugin_editable_select.vue',
'plugin-add-results': 'components/plugin_add_results.vue',
'plugin-auto-label': 'components/plugin_auto_label.vue',
'plugin-file-import-select': 'components/plugin_file_import_select.vue',
'glue-state-sync-wrapper': 'components/glue_state_sync_wrapper.vue'}

_verbosity_levels = ('debug', 'info', 'warning', 'error')
Expand Down
2 changes: 1 addition & 1 deletion jdaviz/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
</gl-row>
</golden-layout>
</pane>
<pane size="25" min-size="25" v-if="state.drawer" style="background-color: #fafbfc; border-top: 6px solid #C75109">
<pane size="25" min-size="25" v-if="state.drawer" style="background-color: #fafbfc; border-top: 6px solid #C75109; min-width: 250px">
<v-card flat tile class="overflow-y-auto fill-height" style="overflow-x: hidden" color="gray">
<v-text-field
v-model='state.tray_items_filter'
Expand Down
71 changes: 71 additions & 0 deletions jdaviz/components/plugin_file_import_select.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<template>
<div>
<v-row>
<v-select
:menu-props="{ left: true }"
attach
:items="items.map(i => i.label)"
v-model="selected"
@change="$emit('update:selected', $event)"
:label="label"
:hint="hint"
persistent-hint
></v-select>
<v-chip v-if="selected === 'From File...'"
close
close-icon="mdi-close"
label
@click:close="() => {this.$emit('click-cancel')}"
style="margin-top: -50px; width: 100%"
>
<!-- @click:close resets from_file and relies on the @observe in python to reset preset
to its default, but the traitlet change wouldn't be fired if from_file is already
empty (which should only happen if setting from the API but not setting from_file) -->
<span style="overflow-x: hidden; whitespace: nowrap; text-overflow: ellipsis; width: 100%">
{{from_file.split("/").slice(-1)[0]}}
</span>
</v-chip>
</v-row>
<v-dialog :value="selected === 'From File...' && from_file.length == 0" height="400" width="600">
<v-card>
<v-card-title class="headline" color="primary" primary-title>{{ dialog_title || "Import File" }}</v-card-title>
<v-card-text>
{{ dialog_hint }}
<v-container>
<v-row>
<v-col>
<slot></slot>
</v-col>
</v-row>
<v-row v-if="from_file_message.length > 0" :style='"color: red"'>
{{from_file_message}}
</v-row>
<v-row v-else>
Valid file
</v-row>
</v-container>
</v-card-text>

<v-card-actions>
<div class="flex-grow-1"></div>
<v-btn color="primary" text @click="$emit('click-cancel')">Cancel</v-btn>
<v-btn color="primary" text @click="$emit('click-import')" :disabled="from_file_message.length > 0">Load</v-btn>
</v-card-actions>

</v-card>
</v-dialog>
</div>
</template>

<script>
module.exports = {
props: ['items', 'selected', 'label', 'hint', 'rules', 'from_file', 'from_file_message',
'dialog_title', 'dialog_hint']
};
</script>

<style>
.v-chip__content {
width: 100%
}
</style>
84 changes: 32 additions & 52 deletions jdaviz/configs/imviz/plugins/catalogs/catalogs.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import os

import numpy as np
import numpy.ma as ma
from astropy import units as u
from astropy.table import QTable
from astropy.coordinates import SkyCoord
from traitlets import List, Unicode, Bool, Int, observe
from traitlets import List, Unicode, Bool, Int

from jdaviz.configs.default.plugins.data_tools.file_chooser import FileChooser
from jdaviz.core.events import SnackbarMessage
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import (PluginTemplateMixin, ViewerSelectMixin,
SelectPluginComponent)
FileImportSelectPluginComponent, HasFileImportSelect)

__all__ = ['Catalogs']


@tray_registry('imviz-catalogs', label="Catalog Search")
class Catalogs(PluginTemplateMixin, ViewerSelectMixin):
class Catalogs(PluginTemplateMixin, ViewerSelectMixin, HasFileImportSelect):
"""
See the :ref:`Catalog Search Plugin Documentation <imviz-catalogs>` for more details.
Expand All @@ -30,65 +27,33 @@ class Catalogs(PluginTemplateMixin, ViewerSelectMixin):
template_file = __file__, "catalogs.vue"
catalog_items = List([]).tag(sync=True)
catalog_selected = Unicode("").tag(sync=True)
from_file = Unicode().tag(sync=True)
from_file_message = Unicode().tag(sync=True)
results_available = Bool(False).tag(sync=True)
number_of_results = Int(0).tag(sync=True)

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

self.catalog = SelectPluginComponent(self,
items='catalog_items',
selected='catalog_selected',
manual_options=['SDSS', 'From File...'])

# file chooser for From File
start_path = os.environ.get('JDAVIZ_START_DIR', os.path.curdir)
self._file_upload = FileChooser(start_path)
self.components = {'g-file-import': self._file_upload}
self._file_upload.observe(self._on_file_path_changed, names='file_path')
self._cached_table_from_file = {}
self._marker_name = 'catalog_results'
self.catalog = FileImportSelectPluginComponent(self,
items='catalog_items',
selected='catalog_selected',
manual_options=['SDSS', 'From File...'])

def _on_file_path_changed(self, event):
self.from_file_message = 'Checking if file is valid'
path = event['new']
if (path is not None
and not os.path.exists(path)
or not os.path.isfile(path)):
self.from_file_message = 'File path does not exist'
return
# set the custom file parser for importing catalogs
self.catalog._file_parser = self._file_parser

self._marker_name = 'catalog_results'

@staticmethod
def _file_parser(path):
try:
table = QTable.read(path)
except Exception:
self.from_file_message = 'Could not parse file with astropy.table.QTable.read'
return
return 'Could not parse file with astropy.table.QTable.read', {}

if 'sky_centroid' not in table.colnames:
self.from_file_message = 'Table does not contain required sky_centroid column'
return
return 'Table does not contain required sky_centroid column', {}

# since we loaded the file already to check if its valid, we might as well cache the table
# so we don't have to re-load it when clicking search. We'll only keep the latest entry
# though, but store in a dict so we can catch if the file path was changed from the API
self._cached_table_from_file = {path: table}
self.from_file_message = ''

@observe('from_file')
def _from_file_changed(self, event):
if len(event['new']):
if not os.path.exists(event['new']):
raise ValueError(f"{event['new']} does not exist")
self.catalog.selected = 'From File...'
else:
# NOTE: select_default will change the value even if the current value is valid
# (so will change from 'From File...' to the first entry in the dropdown)
self.catalog.select_default()

def vue_set_file_from_dialog(self, *args, **kwargs):
self.from_file = self._file_upload.file_path
return '', {path: table}

def search(self):
"""
Expand Down Expand Up @@ -165,7 +130,7 @@ def search(self):
elif self.catalog_selected == 'From File...':
# all exceptions when going through the UI should have prevented setting this path
# but this exceptions might be raised here if setting from_file from the UI
table = self._cached_table_from_file.get(self.from_file, QTable.read(self.from_file))
table = self.catalog.selected_obj
self.app._catalog_source_table = table
skycoord_table = table['sky_centroid']

Expand Down Expand Up @@ -208,6 +173,21 @@ def search(self):

return skycoord_table

def import_catalog(self, catalog):
"""
Import a catalog from a file path.
Parameters
----------
catalog : str
Path to a file that can be parsed by astropy QTable
"""
# TODO: self.catalog.import_obj for a QTable directly (see footprints implementation)
if isinstance(catalog, str):
self.catalog.import_file(catalog)
else: # pragma: no cover
raise ValueError("catalog must be a string (file path)")

def vue_do_search(self, *args, **kwargs):
# calls self.search() which handles all of the searching logic
self.search()
Expand Down
97 changes: 25 additions & 72 deletions jdaviz/configs/imviz/plugins/catalogs/catalogs.vue
Original file line number Diff line number Diff line change
@@ -1,92 +1,45 @@
<template>
<j-tray-plugin
<j-tray-plugin
description='Queries an area encompassed by the viewer using a specified catalog and marks all the objects found within the area.'
:link="'https://jdaviz.readthedocs.io/en/'+vdocs+'/'+config+'/plugins.html#catalog-search'"
:popout_button="popout_button">

<plugin-viewer-select
<plugin-viewer-select
:items="viewer_items"
:selected.sync="viewer_selected"
label="Viewer"
:show_if_single_entry="false"
hint="Select a viewer to search."
/>

<v-row>

<v-select
:menu-props="{ left: true }"
attach
:items="catalog_items.map(i => i.label)"
v-model="catalog_selected"
label="Catalog"
hint="Select a catalog to search with."
persistent-hint
></v-select>
<v-chip v-if="catalog_selected === 'From File...'"
close
close-icon="mdi-close"
label
@click:close="() => {if (from_file.length) {from_file = ''} else {catalog_selected = catalog_items[0].label}}"
style="margin-top: -50px; width: 100%"
>
<!-- @click:close resets from_file and relies on the @observe in python to reset catalog
to its default, but the traitlet change wouldn't be fired if from_file is already
empty (which should only happen if setting from the API but not setting from_file) -->
<span style="overflow-x: hidden; whitespace: nowrap; text-overflow: ellipsis; width: 100%">
{{from_file.split("/").slice(-1)[0]}}
</span>
</v-chip>
</v-row>

<v-dialog :value="catalog_selected === 'From File...' && from_file.length === 0" height="400" width="600">
<v-card>
<v-card-title class="headline" color="primary" primary-title>Load Catalog</v-card-title>
<v-card-text>
Select a file containing a catalog.
<v-container>
<v-row>
<v-col>
<g-file-import id="file-uploader"></g-file-import>
</v-col>
</v-row>
<v-row v-if="from_file_message.length > 0" :style='"color: red"'>
{{from_file_message}}
</v-row>
<v-row v-else>
Valid catalog file
</v-row>
</v-container>
</v-card-text>

<v-card-actions>
<div class="flex-grow-1"></div>
<v-btn color="primary" text @click="catalog_selected = catalog_items[0].label">Cancel</v-btn>
<v-btn color="primary" text @click="set_file_from_dialog" :disabled="from_file_message.length > 0">Load</v-btn>
</v-card-actions>

</v-card>
</v-dialog>

<v-row class="row-no-outside-padding">
/>

<plugin-file-import-select
:items="catalog_items"
:selected.sync="catalog_selected"
label="Catalog"
hint="Select a catalog to search."
:from_file.sync="from_file"
:from_file_message="from_file_message"
dialog_title="Import Catalog"
dialog_hint="Select a file containing a catalog"
@click-cancel="file_import_cancel()"
@click-import="file_import_accept()"
>
<g-file-import id="file-uploader"></g-file-import>
</plugin-file-import-select>

<v-row class="row-no-outside-padding">
<v-col>
<v-btn color="primary" text @click="do_clear">Clear</v-btn>
</v-col>
<v-col>
<v-btn color="primary" text @click="do_search">Search</v-btn>
</v-col>
</v-row>
</v-row>

<v-row>
<v-row>
<p class="font-weight-bold">Results:</p>
<span style='padding-left: 4px' v-if="results_available">{{number_of_results}}</span>
<v-row>
<v-row>

</j-tray-plugin>
</template>

<style scoped>
.v-chip__content {
width: 100%
}
</style>
</j-tray-plugin>
</template>
Loading