+
+ A CoordinateReferenceSystemCombo with autocomplete mode
+ where CRS are fetched from epsg.io/.
+ If a CRS is selected (prop onSelect), the projection is
+ used to perform client-side raster reprojection of OSM layer in map.
+
+
+
+
+
+
+
+
+
+ A CoordinateReferenceSystemCombo with predefined definitions
+ of four CRS. Selecting an option does not affect the map.
+
-
-
- A CoordinateReferenceSystemCombo with autocomplete mode
- where CRS are fetched from epsg.io/.
- If a CRS is selected (prop onSelect), the projection is
- used to perform client-side raster reprojection of OSM layer in map.
-
-
-
-
-
-
-
-
-
- A CoordinateReferenceSystemCombo with predefined definitions
- of four CRS. Selecting an option does not affect the map.
-
-
-
-
- {/* A CoordinateReferenceSystemCombo having predefinedCrsDefinitions*/}
-
-
+
+
+ {/* A CoordinateReferenceSystemCombo having predefinedCrsDefinitions*/}
+
- );
- }
+
+ );
}
diff --git a/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.spec.tsx b/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.spec.tsx
index ce48994dc2..45b4a4f236 100644
--- a/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.spec.tsx
+++ b/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.spec.tsx
@@ -1,14 +1,10 @@
-import Logger from '@terrestris/base-util/dist/Logger';
import { actSetTimeout } from '@terrestris/react-util/dist/Util/rtlTestUtils';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { enableFetchMocks, FetchMock } from 'jest-fetch-mock';
import * as React from 'react';
-import {
- findAntdDropdownOptionByText,
- queryAntdDropdownOption
-} from '../../Util/antdTestQueries';
+import { findAntdDropdownOptionByText, queryAntdDropdownOption } from '../../Util/antdTestQueries';
import CoordinateReferenceSystemCombo from '../CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo';
describe('', () => {
@@ -108,51 +104,8 @@ describe('', () => {
});
});
- describe('error handling', () => {
- it('logs error message', async () => {
- const error = new Error('Peter');
- (fetch as FetchMock).mockRejectOnce(error);
-
- const loggerSpy = jest.spyOn(Logger, 'error');
-
- render();
- const combobox = screen.getByRole('combobox');
- await userEvent.type(combobox, 'a');
-
- await waitFor(() => {
- expect(loggerSpy).toHaveBeenCalled();
- });
-
- expect(loggerSpy).toHaveBeenCalledWith('Error while requesting in CoordinateReferenceSystemCombo', error);
-
- loggerSpy.mockRestore();
- });
- });
-
describe('option clicks are handled correctly', () => {
- it('calls the onSelect callback with the correct value', async () => {
- const onSelect = jest.fn();
-
- render();
-
- const combobox = screen.getByRole('combobox');
-
- await userEvent.type(combobox, 'a');
-
- const result = resultMock.results[0];
- const expected = transformedResults[0];
-
- const option = await findAntdDropdownOptionByText(`${result.name} (EPSG:${result.code})`);
-
- // we have to use fireEvent directly instead of `userEvent.click()` because antd is in the way
- fireEvent.click(option);
-
- await waitFor(() => {
- expect(onSelect).toBeCalledWith(expected);
- });
- });
-
it('sets the value of the combobox to the correct value', async () => {
const onSelect = jest.fn();
diff --git a/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.tsx b/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.tsx
index f9fbb97df4..505f780d2d 100644
--- a/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.tsx
+++ b/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.tsx
@@ -1,35 +1,36 @@
-import { AutoComplete } from 'antd';
-import { AutoCompleteProps } from 'antd/lib/auto-complete';
-import * as React from 'react';
-
-const { Option } = AutoComplete;
-
import './CoordinateReferenceSystemCombo.less';
import Logger from '@terrestris/base-util/dist/Logger';
-import UrlUtil from '@terrestris/base-util/dist/UrlUtil/UrlUtil';
+import {
+ ProjectionDefinition,
+ useProjFromEpsgIO
+} from '@terrestris/react-util/dist/Hooks/useProjFromEpsgIO/useProjFromEpsgIO';
+import { AutoComplete } from 'antd';
+import { AutoCompleteProps } from 'antd/lib/auto-complete';
+import { DefaultOptionType } from 'antd/lib/select';
+import _find from 'lodash/find';
+import _isEqual from 'lodash/isEqual';
+import _isNil from 'lodash/isNil';
+import React, { FC, useEffect, useMemo, useState } from 'react';
import { CSS_PREFIX } from '../../constants';
-interface CrsDefinition {
- value: string;
- code: string;
-}
+const { Option } = AutoComplete;
interface OwnProps {
/**
* The API to query for CRS definitions
* default: https://epsg.io
*/
- crsApiUrl: string;
+ crsApiUrl?: string;
/**
* The empty text set if no value is given / provided
*/
- emptyTextPlaceholderText: string;
+ emptyTextPlaceholderText?: string;
/**
* A function
*/
- onSelect: (crsDefinition: CrsDefinition) => void;
+ onSelect?: (projectionDefinition: ProjectionDefinition | undefined) => void;
/**
* An optional CSS class which should be added.
*/
@@ -38,15 +39,11 @@ interface OwnProps {
* An array of predefined crs definitions having at least value (name of
* CRS) and code (e.g. EPSG-code of CRS) property
*/
- predefinedCrsDefinitions?: CrsDefinition[];
-}
-
-interface CRSComboState {
- crsDefinitions: CrsDefinition[];
- value: string|null;
+ predefinedCrsDefinitions?: Record;
}
export type CRSComboProps = OwnProps & AutoCompleteProps;
+const defaultClassName = `${CSS_PREFIX}coordinatereferencesystemcombo`;
/**
* Class representing a combo to choose coordinate projection system via a
@@ -55,191 +52,112 @@ export type CRSComboProps = OwnProps & AutoCompleteProps;
* @class The CoordinateReferenceSystemCombo
* @extends React.Component
*/
-class CoordinateReferenceSystemCombo extends React.Component {
-
- static defaultProps = {
- emptyTextPlaceholderText: 'Please select a CRS',
- crsApiUrl: 'https://epsg.io/',
- onSelect: () => undefined
- };
-
- /**
- * The className added to this component.
- * @private
- */
- className = `${CSS_PREFIX}coordinatereferencesystemcombo`;
-
- /**
- * Create a CRS combo.
- * @constructs CoordinateReferenceSystemCombo
- */
- constructor(props: CRSComboProps) {
- super(props);
-
- this.state = {
- crsDefinitions: [],
- value: null
- };
- }
-
- /**
- * Fetch CRS definitions from epsg.io for given search string
- *
- * @param searchVal The search string
- */
- fetchCrs = async (searchVal: string) => {
- const { crsApiUrl } = this.props;
-
- const queryParameters = {
- format: 'json',
- q: searchVal
- };
-
- return fetch(`${crsApiUrl}?${UrlUtil.objectToRequestString(queryParameters)}`)
- .then(response => response.json());
- };
+const CoordinateReferenceSystemCombo: FC = ({
+ crsApiUrl,
+ className,
+ emptyTextPlaceholderText = 'Please select a CRS',
+ onSelect = () => undefined,
+ predefinedCrsDefinitions,
+ ...passThroughOpts
+}) => {
+
+ const [projectionDefinitions, setProjectionDefinitions] = useState>({});
+ const [searchValue, setSearchValue] = useState();
+ const [selected, setSelected] = useState();
/**
* This function gets called when the EPSG.io fetch returns an error.
* It logs the error to the console.
*
*/
- onFetchError(error: any) {
- Logger.error('Error while requesting in CoordinateReferenceSystemCombo', error);
- }
-
- /**
- * This function transforms results of EPSG.io
- *
- * @param json The result object of EPSG.io-API, see where
- * https://github.com/klokantech/epsg.io#api-for-results
- * @return Array of CRS definitons used in CoordinateReferenceSystemCombo
- */
- transformResults = (json: any): CrsDefinition[] => {
- const results = json.results;
- if (results && results.length > 0) {
- return results.map((obj: any) => ({code: obj.code, value: obj.name, proj4def: obj.proj4, bbox: obj.bbox}));
- } else {
- return [];
- }
+ const onFetchError = (error: any) => {
+ Logger.error(`Error while requesting in CoordinateReferenceSystemCombo: ${error}`);
};
- /**
- * This function gets called when the EPSG.io fetch returns an error.
- * It logs the error to the console.
- *
- * @param value The search value.
- */
- handleSearch = async (value: string) => {
- const {
- predefinedCrsDefinitions
- } = this.props;
-
- if (!value || value.length === 0) {
- this.setState({
- value,
- crsDefinitions: []
- });
- return;
- }
+ const crsObjects = useMemo(
+ () => predefinedCrsDefinitions || projectionDefinitions,
+ [projectionDefinitions, predefinedCrsDefinitions]
+ );
- if (!predefinedCrsDefinitions) {
- try {
- const result = await this.fetchCrs(value);
- this.setState({
- crsDefinitions: this.transformResults(result)
- });
- } catch (e) {
- this.onFetchError(e);
- }
- } else {
- this.setState({ value });
- }
- };
+ const epsgIoResults = useProjFromEpsgIO({
+ crsApiUrl,
+ onFetchError,
+ searchValue
+ });
+
+ const getEpsgDescription = (projDefinition: ProjectionDefinition) =>
+ `${projDefinition.name} (EPSG:${projDefinition.code})`;
/**
* Handles selection of a CRS item in Autocomplete
*
- * @param value The EPSG code.
+ * @param _
* @param option The selected OptionData
*/
- onCrsItemSelect = (value: string, option: any) => {
- const {
- onSelect,
- predefinedCrsDefinitions
- } = this.props;
-
- const {
- crsDefinitions
- } = this.state;
-
- const crsObjects = predefinedCrsDefinitions || crsDefinitions;
-
- const selected = crsObjects.filter(i => i.code === option.key)[0];
-
- this.setState({
- value: selected.value
- });
-
- onSelect(selected);
+ const onCrsItemSelect = (_: string, option: DefaultOptionType) => {
+ const selectedProjection = _find(crsObjects, (p: ProjectionDefinition) => p.code === option.code);
+ setSelected(selectedProjection);
+ if (!_isNil(selectedProjection)) {
+ setSearchValue(undefined);
+ }
};
/**
- * Tranforms CRS object returned by EPSG.io to antd Option component
+ * Transform CRS object returned by EPSG.io to antd Option component
*
- * @param crsObject Single plain CRS object returned by EPSG.io
+ * @param code The EPSG code of the ProjectionDefinition
+ * @param projDefinition Single plain CRS object returned by EPSG.io
*
* @return Option component to render
*/
- transformCrsObjectsToOptions(crsObject: CrsDefinition) {
- const value = `${crsObject.value} (EPSG:${crsObject.code})`;
-
+ const transformCrsObjectsToOptions = ([, projDefinition]: [string, ProjectionDefinition]) => {
+ const epsgDescription = getEpsgDescription(projDefinition);
return (
);
- }
-
- /**
- * The render function.
- */
- render() {
- const {
- className,
- emptyTextPlaceholderText,
- onSelect,
- crsApiUrl,
- predefinedCrsDefinitions,
- ...passThroughOpts
- } = this.props;
-
- const {
- crsDefinitions
- } = this.state;
+ };
- const crsObjects = predefinedCrsDefinitions || crsDefinitions;
+ const onClear = () => {
+ setSelected(undefined);
+ setSearchValue(undefined);
+ setProjectionDefinitions({});
+ };
- const finalClassName = className ? `${className} ${this.className}` : this.className;
+ useEffect(() => {
+ if (!_isNil(epsgIoResults) && !_isEqual(epsgIoResults, projectionDefinitions) && _isNil(selected)) {
+ setProjectionDefinitions(epsgIoResults);
+ }
+ }, [epsgIoResults, projectionDefinitions, selected]);
- return (
- this.onCrsItemSelect(v, o)}
- onChange={(v: string) => this.handleSearch(v)}
- placeholder={emptyTextPlaceholderText}
- {...passThroughOpts}
- >
- {
- crsObjects.map(this.transformCrsObjectsToOptions)
- }
-
- );
- }
-}
+ useEffect(() => {
+ if (!_isNil(selected)) {
+ onSelect(selected);
+ }
+ }, [onSelect, selected]);
+
+ const finalClassName = className ? `${defaultClassName} ${className}` : defaultClassName;
+
+ return (
+
+ {
+ Object.entries(crsObjects).map(transformCrsObjectsToOptions)
+ }
+
+ );
+};
export default CoordinateReferenceSystemCombo;