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

Updates the search page #10

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
65 changes: 65 additions & 0 deletions src/components/CoordResolverInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<v-text-field
ref="resolverInput"
v-model="searchStore.resolverName"
label="Target name"
placeholder="NGC 4395"
hint="Enter a name of target"
variant="outlined"
density="default"
:error="isErrorStatus"
clearable
@keydown.enter="searchStore.resolveName"
@update:modelValue="searchStore.resetResolver"
>
<template v-slot:append>

<v-btn
:loading="searchStore.resolverIsLoading"
@click="searchStore.resolveName"
variant="outlined"
rounded="5"
class="mr-3"
prepend-icon="mdi-magnify"
>
Resolve
</v-btn>
<v-btn-toggle
v-model="searchStore.resolverServer"
shaped
mandatory
divided
density="compact"
rounded="5">
<v-tooltip
text="Resolve name in CDS Simbad database"
location="bottom"
>
<template v-slot:activator="{ props }">
<v-btn value="simbad" v-bind="props">Simbad</v-btn>
</template>
</v-tooltip>
<v-tooltip
text="Resolve name in NASA/IPAC Extragalactic Database (NED) using CDS Sesame"
location="bottom"
>
<template v-slot:activator="{ props }">
<v-btn value="ned" v-bind="props">NED</v-btn>
</template>
</v-tooltip>
</v-btn-toggle>
</template>
<template v-slot:details>{{ searchStore.resolverMessage }}</template>
</v-text-field>
</template>

<script setup lang="ts">

import { computed } from 'vue'
import { useSearchStore } from '@/store/app'

// get the application state store and router
const searchStore = useSearchStore()
const isErrorStatus = computed(() => searchStore.resolverStatus == 'error')

</script>
83 changes: 83 additions & 0 deletions src/store/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Utilities
import { defineStore } from 'pinia'
import axios, { AxiosResponse } from 'axios'
import {parseSimbadResponse, parseNEDResponse} from '@/utils/parseSimbadNEDResponse'

export const useAppStore = defineStore('app', {
state: () => ({
Expand Down Expand Up @@ -77,3 +79,84 @@ export const useAppStore = defineStore('app', {

}
})


// Split store in logical component. `search` store will contain all data related to search form/process.
export const useSearchStore = defineStore('search', {
state: () => ({
resolverName: '',
resolverServer: 'simbad',
resolverStatus: null,
resolverMessage: '',
resolverCoords: null,
resolverIsLoading: false,
}),
actions: {
resolveName() {

let url: string

// Resolve button clicked without providing the target name
if (this.resolverName == '') {
this.resolverStatus = 'error'
this.resolverMessage = 'Target Name is required'
this.resolverCoords = null
this.resolverIsLoading = false
return
}

// select server resolver
switch (this.resolverServer) {
case 'simbad':
url = `https://simbad.cds.unistra.fr/simbad/sim-id?Ident=${encodeURIComponent(this.resolverName)}&output.format=votable&output.params=main_id,coo(d)`
break
case 'ned':
// NED via Sesame CDS https://vizier.cds.unistra.fr/vizier/doc/sesame.htx
// NED directly does not allow due to CORS
url = `https://cds.unistra.fr/cgi-bin/nph-sesame/-ox/~N?${encodeURIComponent(this.resolverName)}`
break
default:
throw new Error('Unknown name resolver')
}

// set loading status while response is going on
this.resolverIsLoading = true

let res: {status: string, message: string, payload: any}

axios
.get(url)
.then((response) => {
switch (this.resolverServer) {
case 'simbad':
res = parseSimbadResponse(response.data)
break
case 'ned':
res = parseNEDResponse(response.data)
break
}
// successful server response, but status can be `error` if cannot find object in DB
this.resolverStatus = res.status
this.resolverMessage = res.message
this.resolverCoords = res.payload
this.resolverIsLoading = false
})
.catch((error) => {
// error response is something went wrong
this.resolverStatus = 'error'
this.resolverMessage = `"${this.resolverServer}" cannot successfully resolve "${this.resolverName}" to coordinates`
this.resolverCoords = null
this.resolverIsLoading = false
// error details will be posted in console
console.warn(error);
})
},
// cleanup Target Resolver input
resetResolver() {
this.resolverStatus = null,
this.resolverMessage = '',
this.resolverCoords = null,
this.resolverIsLoading = false
}
}
})
76 changes: 76 additions & 0 deletions src/utils/parseSimbadNEDResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
interface ResponsePayload {
status?: string
message: string
payload: any
}


export function parseSimbadResponse(data: any): ResponsePayload {

// Parse the XML string into an XMLDocument
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(data, 'text/xml')

// Find the INFO element within the XML document suggesting a resolution error
const infoElement = xmlDoc.querySelector('INFO')

if (infoElement) {
// Get error information from the INFO element
const errorInfo = infoElement.getAttribute('value')
console.warn(errorInfo)
return { status: 'error', message: `Simbad: ${errorInfo}`, payload: null }

} else {

// Find the relevant elements within the XML document
const tableData = xmlDoc.querySelector('TABLEDATA')
const rowData = tableData.querySelector('TR')
const columns = rowData.querySelectorAll('TD')

// Extract the data from the columns
const name: string = columns[0].textContent?.trim() || '' // Main identifier
const RA_d: number = parseFloat(columns[1].textContent || '') // Right ascension
const DEC_d: number = parseFloat(columns[2].textContent || '') // Declination

// Construct the output object
const output: { name: string, ra: number, dec: number } = { name: name, ra: RA_d, dec: DEC_d }

return {status: 'success', message: 'Resolved successfully', payload: output }
}
}


export function parseNEDResponse(data: any): ResponsePayload {

// Parse the XML string into an XMLDocument
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(data, 'text/xml')

// Get the Resolver element
const resolverElement = xmlDoc.querySelector('Resolver');

// Find the INFO element within the XML document suggesting a resolution error
const infoElement = xmlDoc.querySelector('INFO')

if (infoElement) {
// Get error information from the INFO element
const errorInfo = infoElement.textContent
console.warn(errorInfo)
return { status: 'error', message: `NED: ${errorInfo}`, payload: null }

} else {
// Extract the desired fields
const jra = resolverElement.querySelector('jradeg')
const jde = resolverElement.querySelector('jdedeg')

const name: string = resolverElement.querySelector('oname').textContent
const jradeg: number = parseFloat(jra.textContent || '')
const jdedeg: number = parseFloat(jde.textContent || '')

// Construct the output object
const output: { name: string, ra: number, dec: number } = { name: name, ra: jradeg, dec: jdedeg }

return { status: 'success', message: 'Resolved successfully', payload: output }
}

}
61 changes: 50 additions & 11 deletions src/views/Search.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<template>
<v-form fast-fail @submit.prevent ref="form" validate-on="input">
<v-container>
<v-row class="mt-1">
<v-col>
<CoordResolverInput />
</v-col>
</v-row>
<v-row>
<!-- width of all columns in a row should add up to 12 ; see Vuetify Grid -->
<v-col md=6>
<v-col cols="12" md=6>
<!-- use of the TextInput component -->
<!-- search coordinate field -->
<text-input
Expand All @@ -12,32 +17,35 @@
placeholder="315.014, 35.299"
hint="Enter a RA, Dec coordinate in [decimal or hmsdms] format"
:rules="coordRules"
density="default"
id="coords"
/>
</v-col>
<v-col md=4>
<v-col cols="12" md=3>
<!-- search radius field -->
<text-input
v-model="formData.radius"
label="Search Radius"
placeholder="0.01"
hint="Enter a search radius"
:rules="radiusRules"
density="default"
id="radius"
/>
</v-col>
<v-col md=2>
<v-col cols="12" md=3>
<!-- radius dropdown select -->
<v-select
v-model="formData.units"
label="Unit"
id="unit"
:items="['degree', 'arcmin', 'arcsec']"
density="default"
></v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="4" md="4">
<v-col cols="12" md="6">
<!-- search id field -->
<text-input
v-model="formData.id"
Expand All @@ -46,13 +54,32 @@
hint="Enter an SDSS identifier"
:rules="idRules"
id="id"
density="default"
/>
</v-col>
<v-col cols="4" md="4">
<dropdown-select label="Programs" id="programs" :items="store.programs" v-model="formData.program"/>
<v-col cols="12" md="6">
<v-file-input
v-model="formData.fileTargets"
show-size
label="Upload a list of targets"
density="default"
variant="outlined">
</v-file-input>
</v-col>
<v-col cols="4" md="4">
<dropdown-select label="Cartons" id="cartons" :items="store.cartons" v-model="formData.carton"/>
</v-row>
<v-row>
<v-col cols="12" md="3">
<dropdown-select label="Programs" id="programs" density="default" :items="store.programs" v-model="formData.program"/>
</v-col>
<v-col cols="12" md="3">
<dropdown-select label="Cartons" id="cartons" density="default" :items="store.cartons" v-model="formData.carton"/>
</v-col>
<v-col cols="12" md="3">
<v-btn-toggle multiple divided mandatory rounded="5" density="default" v-model="formData.mapper">
<v-btn value="bhm">BHM</v-btn>
<v-btn value="mwm">MWM</v-btn>
<v-btn value="lwm">LWM</v-btn>
</v-btn-toggle>
</v-col>
</v-row>

Expand Down Expand Up @@ -84,11 +111,14 @@ import axios from 'axios'
import { ref, onMounted, watch } from 'vue'
import TextInput from '@/components/TextInput.vue'
import DropdownSelect from '@/components/DropdownSelect.vue';
import CoordResolverInput from '@/components/CoordResolverInput.vue';
import { useRouter } from 'vue-router';
import { useAppStore } from '@/store/app'
import { useAppStore, useSearchStore } from '@/store/app'
import { storeToRefs } from 'pinia'

// get the application state store and router
const store = useAppStore()
const searchStore = useSearchStore()
const router = useRouter()

// set up a form reference, is the name in v-form ref="form"
Expand Down Expand Up @@ -124,12 +154,14 @@ let initFormData = {
coords: '',
ra: '',
dec: '',
radius: '',
radius: '0.01',
id: '',
units: 'degree',
release: store.release,
carton: '',
program: ''
program: '',
mapper: ['bhm', 'mwm', 'lwm'],
fileTargets: [],
}
// create dynamic bindings to form fields
let formData = ref({ ...initFormData })
Expand All @@ -145,6 +177,13 @@ watch(formData, async () => {
valid.value = formValid.valid; // update the valid state
}, { deep: true }); // deep watch to track nested property changes

// watcher to update coordinate input if name has been successfully resolved to coordinates
const { resolverCoords } = storeToRefs(searchStore)
watch(resolverCoords, () => {
if (resolverCoords.value) {
formData.value.coords = `${resolverCoords.value.ra}, ${resolverCoords.value.dec}`
}
})

// events
async function submit_form(this: any) {
Expand Down
Loading