UNPKG

terriajs

Version:

Geospatial data visualization platform.

338 lines (316 loc) 10.7 kB
import { observer } from "mobx-react"; import { runInAction } from "mobx"; import PropTypes from "prop-types"; import { Component } from "react"; import { Trans, withTranslation } from "react-i18next"; import { Category, DatatabAction } from "../../../../Core/AnalyticEvents/analyticEvents"; import getDataType from "../../../../Core/getDataType"; import TimeVarying from "../../../../ModelMixins/TimeVarying"; import addUserCatalogMember from "../../../../Models/Catalog/addUserCatalogMember"; import addUserFiles from "../../../../Models/Catalog/addUserFiles"; import CatalogMemberFactory from "../../../../Models/Catalog/CatalogMemberFactory"; import createCatalogItemFromFileOrUrl from "../../../../Models/Catalog/createCatalogItemFromFileOrUrl"; import CommonStrata from "../../../../Models/Definition/CommonStrata"; import upsertModelFromJson from "../../../../Models/Definition/upsertModelFromJson"; import Icon from "../../../../Styled/Icon"; import Dropdown from "../../../Generic/Dropdown"; import Loader from "../../../Loader"; import Styles from "./add-data.scss"; import FileInput from "./FileInput"; import { parseCustomMarkdownToReactWithOptions } from "../../../Custom/parseCustomMarkdownToReact"; import loadJson from "../../../../Core/loadJson"; import TerriaError from "../../../../Core/TerriaError"; /** * Add data panel in modal window -> My data tab */ @observer class AddData extends Component { static propTypes = { terria: PropTypes.object, viewState: PropTypes.object, resetTab: PropTypes.func, activeTab: PropTypes.string, // localDataTypes & remoteDataTypes specifies the file types to show in dropdowns for local and remote data uploads. // These default to the lists defined in getDataType.ts // Some external components use these props to customize the types shown. localDataTypes: PropTypes.arrayOf(PropTypes.object), remoteDataTypes: PropTypes.arrayOf(PropTypes.object), onFileAddFinished: PropTypes.func.isRequired, onUrlAddFinished: PropTypes.func.isRequired, t: PropTypes.func.isRequired }; constructor(props) { super(props); const remoteDataTypes = this.props.remoteDataTypes ?? getDataType().remoteDataType; // Automatically suffix supported extension types to localDataType names const localDataTypes = ( this.props.localDataTypes ?? getDataType().localDataType ).map((dataType) => { const extensions = dataType.extensions?.length ? ` (${buildExtensionsList(dataType.extensions)})` : ""; return { ...dataType, name: `${dataType.name}${extensions}` }; }); this.state = { remoteDataTypes, localDataTypes, localDataType: localDataTypes[0], remoteUrl: "", // By default there's no remote url isLoading: false }; } selectLocalOption(option) { this.setState({ localDataType: option }); } selectRemoteOption(option) { runInAction(() => { this.props.viewState.remoteDataType = option; }); } handleUploadFile(e) { this.setState({ isLoading: true }); addUserFiles( e.target.files, this.props.terria, this.props.viewState, this.state.localDataType ).then((addedCatalogItems) => { if (addedCatalogItems && addedCatalogItems.length > 0) { this.props.onFileAddFinished(addedCatalogItems); } this.setState({ isLoading: false }); // reset active tab when file handling is done this.props.resetTab(); }); } async handleUrl(e) { const url = this.state.remoteUrl; e.preventDefault(); this.props.terria.analytics?.logEvent( Category.dataTab, DatatabAction.addDataUrl, url ); this.setState({ isLoading: true }); let promise; if ( !this.props.viewState.remoteDataType || this.props.viewState.remoteDataType.value === "auto" ) { promise = createCatalogItemFromFileOrUrl( this.props.terria, this.state.remoteUrl, this.props.viewState.remoteDataType?.value ); } else if (this.props.viewState.remoteDataType.value === "json") { promise = loadJson(this.state.remoteUrl) .then((data) => { if (data.error) { return Promise.reject(data.error); } this.props.terria.catalog.group .addMembersFromJson(CommonStrata.user, data.catalog) .raiseError(this.props.terria, "Failed to load catalog from file"); }) .then(() => { this.props.onUrlAddFinished(); }) .catch((error) => TerriaError.from(error).raiseError( this.props.terria, `An error occurred trying to add data from URL: ${this.state.remoteUrl}` ) ) .finally(() => { this.setState({ isLoading: false }); }); } else { try { const newItem = upsertModelFromJson( CatalogMemberFactory, this.props.terria, "", CommonStrata.defaults, { type: this.props.viewState.remoteDataType.value, name: url }, {} ).throwIfUndefined({ message: `An error occurred trying to add data from URL: ${url}` }); newItem.setTrait(CommonStrata.user, "url", url); promise = newItem.loadMetadata().then((result) => { if (result.error) { return Promise.reject(result.error); } return Promise.resolve(newItem); }); } catch (e) { promise = Promise.reject(e); } } addUserCatalogMember(this.props.terria, promise).then((addedItem) => { if (addedItem) { this.props.onFileAddFinished([addedItem]); if (TimeVarying.is(addedItem)) { this.props.terria.timelineStack.addToTop(addedItem); } } // FIXME: Setting state here might result in a react warning if the // component unmounts before the promise finishes this.setState({ isLoading: false }); this.props.resetTab(); }); } onRemoteUrlChange(event) { this.setState({ remoteUrl: event.target.value }); } renderPanels() { const { t } = this.props; const dropdownTheme = { dropdown: Styles.dropdown, list: Styles.dropdownList, isOpen: Styles.dropdownListIsOpen, icon: <Icon glyph={Icon.GLYPHS.opened} /> }; const dataTypes = this.state.localDataTypes.reduce(function ( result, currentDataType ) { if (currentDataType.extensions) { return result.concat( currentDataType.extensions.map((extension) => "." + extension) ); } else { return result; } }, []); const remoteDataType = this.props.viewState.remoteDataType ?? this.state.remoteDataTypes[0]; return ( <div className={Styles.tabPanels}> {this.props.activeTab === "local" && ( <> <div className={Styles.tabHeading}>{t("addData.localAdd")}</div> <section className={Styles.tabPanel}> <label className={Styles.label}> <Trans i18nKey="addData.localFileType"> <strong>Step 1:</strong> Select file type </Trans> </label> <Dropdown options={this.state.localDataTypes} selected={this.state.localDataType} selectOption={this.selectLocalOption.bind(this)} matchWidth theme={dropdownTheme} /> {this.state.localDataType?.description ? parseCustomMarkdownToReactWithOptions( this.state.localDataType?.description ) : null} <label className={Styles.label}> <Trans i18nKey="addData.localFile"> <strong>Step 2:</strong> Select file </Trans> </label> <FileInput accept={dataTypes.join(",")} onChange={this.handleUploadFile.bind(this)} /> {this.state.isLoading && <Loader />} </section> </> )} {this.props.activeTab === "web" && ( <> <div className={Styles.tabHeading}>{t("addData.webAdd")}</div> <section className={Styles.tabPanel}> <label className={Styles.label}> <Trans i18nKey="addData.webFileType"> <strong>Step 1:</strong> Select file or web service type </Trans> </label> <Dropdown options={this.state.remoteDataTypes} selected={remoteDataType} selectOption={this.selectRemoteOption.bind(this)} matchWidth theme={dropdownTheme} /> {remoteDataType?.description ? parseCustomMarkdownToReactWithOptions( remoteDataType?.description ) : null} {remoteDataType?.customComponent ? this.renderCustomComponent(remoteDataType?.customComponent) : this.renderDefaultForWebDataType(t)} </section> </> )} </div> ); } renderCustomComponent(CustomComponent) { return <CustomComponent />; } renderDefaultForWebDataType(t) { return ( <> <label className={Styles.label}> <Trans i18nKey="addData.webFile"> <strong>Step 2:</strong> Enter the URL of the data file or web service </Trans> </label> <form className={Styles.urlInput}> <input value={this.state.remoteUrl} onChange={this.onRemoteUrlChange.bind(this)} className={Styles.urlInputTextBox} type="text" placeholder="e.g. http://data.gov.au/geoserver/wms" /> <button disabled={this.state.remoteUrl.length === 0} type="submit" onClick={this.handleUrl.bind(this)} className={Styles.urlInputBtn} > {t("addData.urlInputBtn")} </button> {this.state.isLoading && <Loader />} </form> </> ); } render() { return <div className={Styles.inner}>{this.renderPanels()}</div>; } } /** * @param extensions - string[] * @returns Comma separated string of extensions */ function buildExtensionsList(extensions) { return extensions.map((ext) => `.${ext}`).join(", "); } export default withTranslation()(AddData);