Skip to content

Commit

Permalink
Footprints: custom regions import (#2377)
Browse files Browse the repository at this point in the history
* fix error message formatting

* WIP: generalize file import component and adopt in footprints plugin

* put implementation in select component

* when clicking cancel, revert to previous selection (not always default)

* improve public API for selecting file

* support loading region from file in footprints plugin

* currently without any repositioning support (region is static)

* update changelog entry

* support importing from API

* update tests/docs

* fix support for multiple file/objects across multiple viewers

* ensure region(s) are sky, not pixel

* fix traceback for parsing regions when file dialog open

* set min-width of plugin tray to 250px

* already set to have a min-width of 25% the app-width, but for small screens/browsers, this will impose a pixel minimum to prevent UI over-crowding (and in this case, allow us to assume the hint in the "preset" dropdown will remain on a single line).

* support for non-polygon region objects

Co-authored-by: P. L. Lim <[email protected]>

* reset internal cache when API/file input is changed

* allow file/object import without pysiaf

* instead of disabling the plugin, options are removed from the preset dropdown and a warning is shown.  
* to avoid "From File..." from being the default, a "None" entry is added if pysiaf is not installed
* this may largely be reverted in the future

Co-authored-by: P. L. Lim <[email protected]>

* Apply suggestions from code review

Co-authored-by: P. L. Lim <[email protected]>

* fix importing region before ever opening plugin

---------

Co-authored-by: P. L. Lim <[email protected]>
  • Loading branch information
kecnry and pllim authored Aug 28, 2023
1 parent 2e61f48 commit 16100af
Show file tree
Hide file tree
Showing 14 changed files with 539 additions and 257 deletions.
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

0 comments on commit 16100af

Please sign in to comment.