diff --git a/app/@types/configContext.d.ts b/app/@types/configContext.d.ts index 0edd9eb..f88fa4f 100644 --- a/app/@types/configContext.d.ts +++ b/app/@types/configContext.d.ts @@ -4,12 +4,14 @@ interface IterableInterface { interface ITableConfig extends IterableInterface { type: 'local' | 'remote', - file: File | null, - url: string | null, + file?: File, + url?: string, selectedColumnsId: number[] columns: string[], - raIndex: number | null, - decIndex: number | null, + raIndex?: number, + decIndex?: number, + raCol?: string, + decCol?: string, state: 'unloaded' | 'loading' | 'success' | 'positionNotFound' | 'error', isSameFile: boolean, dataTypes?: string[], diff --git a/app/components/setup/ConfigForm.tsx b/app/components/setup/ConfigForm.tsx index 5f6dd52..fe58037 100644 --- a/app/components/setup/ConfigForm.tsx +++ b/app/components/setup/ConfigForm.tsx @@ -122,8 +122,8 @@ export default function ConfigForm() { variant="success" size="lg" onClick={handleLoadClick} - disabled={tcState.table.status !== 'success'}> - {tcState.table.status === 'loading' ? + disabled={tcState.table.state !== 'success'}> + {tcState.table.state === 'loading' ? <> void }) { - const { tcState } = useXTableConfig() - const inputRef = useRef(null) - - useEffect(() => { - if (!!tcState.table.file && !!inputRef.current) { - const dataTransfer = new DataTransfer() - dataTransfer.items.add(tcState.table.file) - const fileList = dataTransfer.files - inputRef.current.files = fileList - } - }, [tcState.table.file]) - - return ( - <> - - - Table - - -
- - - Load a table available in local computer. The only required - columns are RA and DEC in degrees.
- Available formars: CSV, TSV, -  DAT, PARQUET. -
-
- -
- - ) -} - - -function RemoteStorageControl({ onChange }: { onChange: (e: any) => void }) { - const { tcState } = useXTableConfig() - - return ( - - - URL - - -
- - - Loads a table available remotely in the internet.
- Available formars: CSV, TSV, -  DAT, PARQUET. -
-
- -
- ) -} +import LocalFileInput from './LocalFileInput' +import RemoteFileInput from './RemoteFileInput' +import InputGroup from 'react-bootstrap/InputGroup' const StateMessage = ({ state }: { state: any }) => { if (state == 'loading') { - return

- Loading table... -

+ return ( +

+ Loading table... +

+ ) } if (state == 'success') { - return

- RA and DEC columns successfully detected -

+ return ( +

+ RA and DEC columns successfully detected +

+ ) } if (state == 'positionNotFound') { - return

- RA or DEC columns not detected -

+ return ( +

+ RA or DEC columns not detected +

+ ) } if (state == 'error') { - return

- Failed to load this table, check if it's a valid csv file -

+ return ( +

+ Failed to load this table, check if it's a valid csv file +

+ ) } return null } +function PositionColumns() { + const { tcState, tcDispatch } = useXTableConfig() + + const handleRAChange: React.ChangeEventHandler = (e) => { + const selectedIndex = parseInt(e.target.value) + let selectedCols = [...tcState.table.selectedColumnsId] + if (tcState.table.raIndex != undefined && selectedCols.includes(tcState.table.raIndex)) { + selectedCols = selectedCols.filter((e) => e != tcState.table.raIndex) + } + if (!selectedCols.includes(selectedIndex)) { + selectedCols.push(selectedIndex) + } + tcDispatch({ + type: ContextActions.USER_FILE_INPUT, + payload: { + raIndex: selectedIndex, + raCol: tcState.table.columns?.[e.target.value as unknown as number], + selectedColumnsId: selectedCols, + state: selectedIndex >= 0 && tcState.table.decIndex != undefined && tcState.table.decIndex >= 0 ? 'success' : tcState.table.state + } + }) + } + + const handleDECChange: React.ChangeEventHandler = (e) => { + const selectedIndex = parseInt(e.target.value) + let selectedCols = [...tcState.table.selectedColumnsId] + if (tcState.table.decIndex != undefined && selectedCols.includes(tcState.table.decIndex)) { + selectedCols = selectedCols.filter((e) => e != tcState.table.decIndex) + } + if (!selectedCols.includes(selectedIndex)) { + selectedCols.push(selectedIndex) + } + tcDispatch({ + type: ContextActions.USER_FILE_INPUT, + payload: { + decIndex: selectedIndex, + decCol: tcState.table.columns?.[e.target.value as unknown as number], + selectedColumnsId: selectedCols, + state: selectedIndex >= 0 && tcState.table.raIndex != undefined && tcState.table.raIndex >= 0 ? 'success' : tcState.table.state + } + }) + } + + return ( + + + { } + + + + RA + = 0} + value={tcState.table.raIndex as unknown as number} + onChange={handleRAChange}> + + { + tcState.table.columns.map((col, i) => ( + + )) + } + + + RA column not found + + + RA column detected + + + + + + + DEC + = 0} + value={tcState.table.decIndex as unknown as number} + onChange={handleDECChange}> + + { + tcState.table.columns.map((col, i) => ( + + )) + } + + + DEC column not found + + + DEC column detected + + + + + ) +} + const SourceSelector = () => { const { tcState, tcDispatch } = useXTableConfig() @@ -164,7 +211,6 @@ const SourceSelector = () => { } - function ColumnButton({ colName, colId }: { colName: string, colId: number }) { const { tcState, tcDispatch } = useXTableConfig() const cls = tcState.table.selectedColumnsId.includes(colId) ? 'btn-primary' : 'btn-outline-primary' @@ -197,188 +243,11 @@ function ColumnButton({ colName, colId }: { colName: string, colId: number }) { } -export default function FileInputTab() { - const { tcState, tcDispatch } = useXTableConfig() - - const handleLocalFile = useCallback((e: any) => { - if (e.target.files.length > 0) { - const file = e.target.files[0] - - tcDispatch({ - type: ContextActions.USER_FILE_INPUT, - payload: { - status: 'loading' - } - }) - - getTableReader(file)?.getTableSummary().then(summary => { - console.log('positionFound', summary?.positionFound) - if (summary?.positionFound) { - const isSameFile = ( - tcState.table.type === 'local' && ( - file.name === tcState.table.file?.name || - file.size === tcState.table.file?.size || - file.lastModified === tcState.table.file?.lastModified - ) - ) - if (!isSameFile) { - tcDispatch({ - type: ContextActions.GRID_UPDATE, - payload: { - data: [], - colDefs: [], - } - }) - } - tcDispatch({ - type: ContextActions.USER_FILE_INPUT, - payload: { - type: 'local', - columns: summary.columns, - selectedColumnsId: [summary.raIndex, summary.decIndex], - raIndex: summary.raIndex, - decIndex: summary.decIndex, - dataTypes: summary.dataTypes, - status: 'success', - file, - isSameFile, - } - }) - tcDispatch({ - type: ContextActions.PLOT_SETUP, - payload: { - filterIndex: [], - filterView: undefined, - inspectSelected: false, - } - }) - event( - 'load_file_local', { - category: 'load', - label: 'local', - userId: GA_MEASUREMENT_ID - }) - } else { - tcDispatch({ - type: ContextActions.USER_FILE_INPUT, - payload: { - status: 'positionNotFound' - } - }) - } - }).catch(err => { - console.log(err) - tcDispatch({ - type: ContextActions.USER_FILE_INPUT, - payload: { - status: 'error' - } - }) - }) - } - }, [tcState, tcDispatch]) - - - const handleRemoteFile = useCallback((url: string, autoLoad: boolean = false) => { - if (url.length > 0) { - tcDispatch({ - type: ContextActions.USER_FILE_INPUT, - payload: { - status: 'loading' - } - }) - - getTableReader(url)?.getTableSummary().then(summary => { - if (summary?.positionFound) { - const isSameFile = ( - tcState.table.type === 'remote' && - url == tcState.table.url - ) - if (!isSameFile) { - tcDispatch({ - type: ContextActions.GRID_UPDATE, - payload: { - data: [], - colDefs: [], - } - }) - } - tcDispatch({ - type: ContextActions.USER_FILE_INPUT, - payload: { - type: 'remote', - columns: summary.columns, - selectedColumnsId: [summary.raIndex, summary.decIndex], - raIndex: summary.raIndex, - decIndex: summary.decIndex, - dataTypes: summary.dataTypes, - status: 'success', - url, - isSameFile, - } - }) - tcDispatch({ - type: ContextActions.PLOT_SETUP, - payload: { - filterIndex: [], - filterView: undefined, - inspectSelected: false, - } - }) - if (autoLoad) { - tcDispatch({ - type: ContextActions.CURRENT_VIEW_CHANGE, - payload: 'grid' - }) - } - event( - 'load_file_remote', { - category: 'load', - label: 'remote', - userId: GA_MEASUREMENT_ID - }) - } else { - tcDispatch({ - type: ContextActions.USER_FILE_INPUT, - payload: { - status: 'positionNotFound' - } - }) - } - }).catch(err => { - tcDispatch({ - type: ContextActions.USER_FILE_INPUT, - payload: { - status: 'error' - } - }) - }) - } - }, [tcDispatch, tcState]) - - useEffect(() => { - Emitter.on('INSERT_URL', (e: any) => handleRemoteFile(e.url, true)) - event( - 'load_file_example', { - category: 'load', - label: 'example', - userId: GA_MEASUREMENT_ID - }) - }, [handleRemoteFile]) +function FileInputColumns() { + const { tcState } = useXTableConfig() return ( <> - - - { - tcState.table.type == 'local' ? - : - handleRemoteFile(e.target.value)} /> - } - - - - { tcState.table.columns && tcState.table.columns.length > 0 && ( @@ -403,4 +272,28 @@ export default function FileInputTab() { } ) +} + + +export default function FileInputTab() { + const { tcState } = useXTableConfig() + + return ( + <> + + +
+ +
+
+ +
+ +
+ +
+ + + + ) } \ No newline at end of file diff --git a/app/components/setup/LocalFileInput.tsx b/app/components/setup/LocalFileInput.tsx new file mode 100644 index 0000000..f6e118e --- /dev/null +++ b/app/components/setup/LocalFileInput.tsx @@ -0,0 +1,138 @@ +import { useXTableConfig } from '@/contexts/XTableConfigContext' +import { useCallback, useEffect, useRef } from 'react' +import Col from 'react-bootstrap/Col' +import Form from 'react-bootstrap/Form' +import Help from '@/components/common/Help' +import Row from 'react-bootstrap/Row' +import { getTableReader } from '@/lib/io' +import { ContextActions } from '@/interfaces/contextActions' +import { GA_MEASUREMENT_ID } from '@/lib/gtag' +import { event } from 'nextjs-google-analytics' + + +export default function LocalFileInput() { + const { tcState, tcDispatch } = useXTableConfig() + const inputRef = useRef(null) + + useEffect(() => { + if (!!tcState.table.file && !!inputRef.current) { + const dataTransfer = new DataTransfer() + dataTransfer.items.add(tcState.table.file) + const fileList = dataTransfer.files + inputRef.current.files = fileList + } + }, [tcState.table.file]) + + const handleLocalFile = useCallback((e: any) => { + if (e.target.files.length > 0) { + const file = e.target.files[0] + + tcDispatch({ + type: ContextActions.USER_FILE_INPUT, + payload: { + state: 'loading' + } + }) + + getTableReader(file)?.getTableSummary().then(summary => { + console.log('summary', summary) + if (summary !== undefined) { + const isSameFile = ( + tcState.table.type === 'local' && ( + file.name === tcState.table.file?.name || + file.size === tcState.table.file?.size || + file.lastModified === tcState.table.file?.lastModified + ) + ) + if (!isSameFile) { + tcDispatch({ + type: ContextActions.GRID_UPDATE, + payload: { + data: [], + colDefs: [], + } + }) + } + const selCols = [] + if (summary.raIndex !== undefined && summary.raIndex >= 0) { + selCols.push(summary.raIndex) + } + if (summary.decIndex !== undefined && summary.decIndex >= 0) { + selCols.push(summary.decIndex) + } + tcDispatch({ + type: ContextActions.USER_FILE_INPUT, + payload: { + type: 'local', + columns: summary.columns, + selectedColumnsId: selCols, + raIndex: summary.raIndex, + decIndex: summary.decIndex, + raCol: summary.raCol, + decCol: summary.decCol, + dataTypes: summary.dataTypes, + state: summary.positionFound ? 'success' : 'positionNotFound', + file, + isSameFile, + } + }) + tcDispatch({ + type: ContextActions.PLOT_SETUP, + payload: { + filterIndex: [], + filterView: undefined, + inspectSelected: false, + } + }) + event( + 'load_file_local', { + category: 'load', + label: 'local', + userId: GA_MEASUREMENT_ID + }) + } + }).catch(err => { + console.log(err) + tcDispatch({ + type: ContextActions.USER_FILE_INPUT, + payload: { + type: 'local', + columns: [], + selectedColumnsId: [], + raIndex: -1, + decIndex: -1, + raCol: undefined, + decCol: undefined, + dataTypes: undefined, + state: 'error', + file: undefined, + } + }) + }) + } + }, [tcState, tcDispatch]) + + return ( + <> + + + Table + + +
+ + + Load a table available in local computer. The only required + columns are RA and DEC in degrees.
+ Available formars: CSV, TSV, +  DAT, PARQUET. +
+
+ +
+ + ) +} \ No newline at end of file diff --git a/app/components/setup/RemoteFileInput.tsx b/app/components/setup/RemoteFileInput.tsx new file mode 100644 index 0000000..0633662 --- /dev/null +++ b/app/components/setup/RemoteFileInput.tsx @@ -0,0 +1,138 @@ +import Help from '@/components/common/Help' +import { useXTableConfig } from '@/contexts/XTableConfigContext' +import { ContextActions } from '@/interfaces/contextActions' +import Emitter from '@/lib/Emitter' +import { GA_MEASUREMENT_ID } from '@/lib/gtag' +import { getTableReader } from '@/lib/io' +import { isUrlValid } from '@/lib/utils' +import { event } from 'nextjs-google-analytics' +import { useCallback, useEffect } from 'react' +import Col from 'react-bootstrap/Col' +import Form from 'react-bootstrap/Form' +import Row from 'react-bootstrap/Row' + + + +export default function RemoteFileInput() { + const { tcState, tcDispatch } = useXTableConfig() + + const handleRemoteFile = useCallback((url: string, autoLoad: boolean = false) => { + if (url.length > 0 && isUrlValid(url)) { + tcDispatch({ + type: ContextActions.USER_FILE_INPUT, + payload: { + state: 'loading' + } + }) + + getTableReader(url)?.getTableSummary().then(summary => { + if (summary != undefined) { + const isSameFile = ( + tcState.table.type === 'remote' && + url == tcState.table.url + ) + if (!isSameFile) { + tcDispatch({ + type: ContextActions.GRID_UPDATE, + payload: { + data: [], + colDefs: [], + } + }) + } + const selCols = [] + if (summary.raIndex !== undefined && summary.raIndex >= 0) { + selCols.push(summary.raIndex) + } + if (summary.decIndex !== undefined && summary.decIndex >= 0) { + selCols.push(summary.decIndex) + } + tcDispatch({ + type: ContextActions.USER_FILE_INPUT, + payload: { + type: 'remote', + columns: summary.columns, + selectedColumnsId: selCols, + raIndex: summary.raIndex, + decIndex: summary.decIndex, + raCol: summary.raCol, + decCol: summary.decCol, + dataTypes: summary.dataTypes, + state: summary.positionFound ? 'success' : 'positionNotFound', + url, + isSameFile, + } + }) + tcDispatch({ + type: ContextActions.PLOT_SETUP, + payload: { + filterIndex: [], + filterView: undefined, + inspectSelected: false, + } + }) + if (autoLoad) { + tcDispatch({ + type: ContextActions.CURRENT_VIEW_CHANGE, + payload: 'grid' + }) + } + event( + 'load_file_remote', { + category: 'load', + label: 'remote', + userId: GA_MEASUREMENT_ID + }) + } + }).catch(err => { + console.log(err) + tcDispatch({ + type: ContextActions.USER_FILE_INPUT, + payload: { + type: 'remote', + columns: [], + selectedColumnsId: [], + raIndex: -1, + decIndex: -1, + raCol: undefined, + decCol: undefined, + dataTypes: undefined, + state: 'error', + file: undefined, + } + }) + }) + } + }, [tcDispatch, tcState]) + + useEffect(() => { + Emitter.on('INSERT_URL', (e: any) => handleRemoteFile(e.url, true)) + event( + 'load_file_example', { + category: 'load', + label: 'example', + userId: GA_MEASUREMENT_ID + }) + }, [handleRemoteFile]) + + return ( + + + URL + + +
+ handleRemoteFile(e.target.value)} + value={tcState.table.url || ''} /> + + Loads a table available remotely in the internet.
+ Available formars: CSV, TSV, +  DAT, PARQUET. +
+
+ +
+ ) +} \ No newline at end of file diff --git a/app/contexts/XTableConfigContext.tsx b/app/contexts/XTableConfigContext.tsx index 5b0dfec..552ee5a 100644 --- a/app/contexts/XTableConfigContext.tsx +++ b/app/contexts/XTableConfigContext.tsx @@ -3,18 +3,20 @@ import localforage from 'localforage' import { ContextActions } from '@/interfaces/contextActions' -export const SCHEMA_VERSION: number = 21 +export const SCHEMA_VERSION: number = 22 const getInitialState = (): IState => ({ schemaVersion: SCHEMA_VERSION, table: { type: 'local', - file: null, - url: null, + file: undefined, + url: undefined, selectedColumnsId: [], columns: [], - raIndex: null, - decIndex: null, + raIndex: undefined, + decIndex: undefined, + raCol: undefined, + decCol: undefined, state: 'unloaded', isSameFile: false, dataTypes: undefined, diff --git a/app/lib/csv.ts b/app/lib/csv.ts index b37c34c..397ad04 100644 --- a/app/lib/csv.ts +++ b/app/lib/csv.ts @@ -16,6 +16,7 @@ export class CSVReader extends BaseReader { Papa.parse(this.file, { complete: ({ data }) => resolve(data), error: (e) => reject(e), + delimitersToGuess: [',', '\t', ' ', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP], skipEmptyLines: true, header: true, dynamicTyping: true, diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 56d7076..0d16512 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -57,4 +57,50 @@ export function findIndex(query: string, items: string[]) { return items.findIndex((item, i) => { return regex.exec(item) !== null }) +} + + +export function isUrlValid(url: string) { + const urlRE = new RegExp( + "^" + + // protocol identifier (optional) + // short syntax // still required + "(?:(?:(?:https?|ftp):)?\\/\\/)" + + // user:pass BasicAuth (optional) + "(?:\\S+(?::\\S*)?@)?" + + "(?:" + + // IP address exclusion + // private & local networks + "(?!(?:10|127)(?:\\.\\d{1,3}){3})" + + "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" + + "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" + + // IP address dotted notation octets + // excludes loopback network 0.0.0.0 + // excludes reserved space >= 224.0.0.0 + // excludes network & broadcast addresses + // (first & last IP address of each class) + "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + + "|" + + // host & domain names, may end with dot + // can be replaced by a shortest alternative + // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ + "(?:" + + "(?:" + + "[a-z0-9\\u00a1-\\uffff]" + + "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + + ")?" + + "[a-z0-9\\u00a1-\\uffff]\\." + + ")+" + + // TLD identifier name, may end with dot + "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + + ")" + + // port number (optional) + "(?::\\d{2,5})?" + + // resource path (optional) + "(?:[/?#]\\S*)?" + + "$", "i" + ) + return urlRE.test(url) } \ No newline at end of file diff --git a/package.json b/package.json index a2d54f9..7427e0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "astroinspect", - "version": "1.4", + "version": "1.5", "private": true, "scripts": { "dev": "next dev -p 3001",