[Maps] implemention of usng support fixing #76144 (#107835)

* implemention of usng support fixing #76144

* linting fixes

* lint fixes

* pr code edits

* fixed utm zone and added error proofing

* removing any data types and replacing with explicit ones

* avoiding render of unused components

* fixing linting issues

Co-authored-by: Michael Ihde <mihde@spectric.com>
This commit is contained in:
Michael Kellogg 2021-09-09 17:52:19 -04:00 committed by GitHub
parent 6803fb362a
commit 5acd1326c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 608 additions and 32 deletions

View file

@ -398,6 +398,7 @@
"typescript-fsa-reducers": "^1.2.2",
"unified": "^9.2.1",
"use-resize-observer": "^6.0.0",
"usng.js": "^0.4.5",
"utility-types": "^3.10.0",
"uuid": "3.3.2",
"vega": "^5.19.1",

View file

@ -5,23 +5,52 @@
* 2.0.
*/
import React, { ChangeEvent, Component } from 'react';
import React, { ChangeEvent, Component, Fragment } from 'react';
import {
EuiForm,
EuiFormRow,
EuiButton,
EuiFieldNumber,
EuiFieldText,
EuiButtonIcon,
EuiPopover,
EuiTextAlign,
EuiSpacer,
EuiPanel,
} from '@elastic/eui';
import { EuiButtonEmpty } from '@elastic/eui';
import { EuiRadioGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import * as usng from 'usng.js';
import { isNaN, isNull } from 'lodash';
import { MapCenter } from '../../../../common/descriptor_types';
import { MapSettings } from '../../../reducers/map';
export const COORDINATE_SYSTEM_DEGREES_DECIMAL = 'dd';
export const COORDINATE_SYSTEM_MGRS = 'mgrs';
export const COORDINATE_SYSTEM_UTM = 'utm';
export const DEFAULT_SET_VIEW_COORDINATE_SYSTEM = COORDINATE_SYSTEM_DEGREES_DECIMAL;
// @ts-ignore
const converter = new usng.Converter();
const COORDINATE_SYSTEMS = [
{
id: COORDINATE_SYSTEM_DEGREES_DECIMAL,
label: 'Degrees Decimal',
},
{
id: COORDINATE_SYSTEM_UTM,
label: 'UTM',
},
{
id: COORDINATE_SYSTEM_MGRS,
label: 'MGRS',
},
];
export interface Props {
settings: MapSettings;
zoom: number;
@ -34,6 +63,17 @@ interface State {
lat: number | string;
lon: number | string;
zoom: number | string;
coord: string;
mgrs: string;
utm: {
northing: string;
easting: string;
zoneNumber: string;
zoneLetter: string | undefined;
zone: string;
};
isCoordPopoverOpen: boolean;
prevView: string | undefined;
}
export class SetViewControl extends Component<Props, State> {
@ -42,8 +82,39 @@ export class SetViewControl extends Component<Props, State> {
lat: 0,
lon: 0,
zoom: 0,
coord: DEFAULT_SET_VIEW_COORDINATE_SYSTEM,
mgrs: '',
utm: {
northing: '',
easting: '',
zoneNumber: '',
zoneLetter: '',
zone: '',
},
isCoordPopoverOpen: false,
prevView: '',
};
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
const nextView = getViewString(nextProps.center.lat, nextProps.center.lon, nextProps.zoom);
const utm = convertLatLonToUTM(nextProps.center.lat, nextProps.center.lon);
const mgrs = convertLatLonToMGRS(nextProps.center.lat, nextProps.center.lon);
if (nextView !== prevState.prevView) {
return {
lat: nextProps.center.lat,
lon: nextProps.center.lon,
zoom: nextProps.zoom,
utm,
mgrs,
prevView: nextView,
};
}
return null;
}
_togglePopover = () => {
if (this.state.isPopoverOpen) {
this._closePopover();
@ -64,6 +135,12 @@ export class SetViewControl extends Component<Props, State> {
});
};
_onCoordinateSystemChange = (coordId: string) => {
this.setState({
coord: coordId,
});
};
_onLatChange = (evt: ChangeEvent<HTMLInputElement>) => {
this._onChange('lat', evt);
};
@ -73,15 +150,144 @@ export class SetViewControl extends Component<Props, State> {
};
_onZoomChange = (evt: ChangeEvent<HTMLInputElement>) => {
this._onChange('zoom', evt);
const sanitizedValue = parseFloat(evt.target.value);
this.setState({
['zoom']: isNaN(sanitizedValue) ? '' : sanitizedValue,
});
};
_onChange = (name: 'lat' | 'lon' | 'zoom', evt: ChangeEvent<HTMLInputElement>) => {
_onUTMZoneChange = (evt: ChangeEvent<HTMLInputElement>) => {
this._onUTMChange('zone', evt);
};
_onUTMEastingChange = (evt: ChangeEvent<HTMLInputElement>) => {
this._onUTMChange('easting', evt);
};
_onUTMNorthingChange = (evt: ChangeEvent<HTMLInputElement>) => {
this._onUTMChange('northing', evt);
};
_onMGRSChange = (evt: ChangeEvent<HTMLInputElement>) => {
this.setState(
{
['mgrs']: isNull(evt.target.value) ? '' : evt.target.value,
},
this._syncToMGRS
);
};
_onUTMChange = (name: 'easting' | 'northing' | 'zone', evt: ChangeEvent<HTMLInputElement>) => {
const value = evt.target.value;
const updateObj = { ...this.state.utm };
updateObj[name] = isNull(value) ? '' : value;
if (name === 'zone' && value.length > 0) {
const zoneLetter = value.substring(value.length - 1);
const zoneNumber = value.substring(0, value.length - 1);
updateObj.zoneLetter = isNaN(zoneLetter) ? zoneLetter : '';
updateObj.zoneNumber = isNaN(zoneNumber) ? '' : zoneNumber;
}
this.setState(
{
// @ts-ignore
['utm']: updateObj,
},
this._syncToUTM
);
};
_onChange = (name: 'lat' | 'lon', evt: ChangeEvent<HTMLInputElement>) => {
const sanitizedValue = parseFloat(evt.target.value);
// @ts-expect-error
this.setState({
[name]: isNaN(sanitizedValue) ? '' : sanitizedValue,
});
this.setState(
// @ts-ignore
{
[name]: isNaN(sanitizedValue) ? '' : sanitizedValue,
},
this._syncToLatLon
);
};
/**
* Sync all coordinates to the lat/lon that is set
*/
_syncToLatLon = () => {
if (this.state.lat !== '' && this.state.lon !== '') {
const utm = convertLatLonToUTM(this.state.lat, this.state.lon);
const mgrs = convertLatLonToMGRS(this.state.lat, this.state.lon);
this.setState({ mgrs, utm });
} else {
this.setState({
mgrs: '',
utm: { northing: '', easting: '', zoneNumber: '', zoneLetter: '', zone: '' },
});
}
};
/**
* Sync the current lat/lon to MGRS that is set
*/
_syncToMGRS = () => {
if (this.state.mgrs !== '') {
let lon;
let lat;
try {
const { north, east } = convertMGRStoLL(this.state.mgrs);
lat = north;
lon = east;
} catch (err) {
return;
}
const utm = convertLatLonToUTM(lat, lon);
this.setState({
lat: isNaN(lat) ? '' : lat,
lon: isNaN(lon) ? '' : lon,
utm,
});
} else {
this.setState({
lat: '',
lon: '',
utm: { northing: '', easting: '', zoneNumber: '', zoneLetter: '', zone: '' },
});
}
};
/**
* Sync the current lat/lon to UTM that is set
*/
_syncToUTM = () => {
if (this.state.utm) {
let lat;
let lon;
try {
({ lat, lon } = converter.UTMtoLL(
this.state.utm.northing,
this.state.utm.easting,
this.state.utm.zoneNumber
));
} catch (err) {
return;
}
const mgrs = convertLatLonToMGRS(lat, lon);
this.setState({
lat: isNaN(lat) ? '' : lat,
lon: isNaN(lon) ? '' : lon,
mgrs,
});
} else {
this.setState({
lat: '',
lon: '',
mgrs: '',
});
}
};
_renderNumberFormRow = ({
@ -117,6 +323,178 @@ export class SetViewControl extends Component<Props, State> {
};
};
_renderMGRSFormRow = ({
value,
onChange,
label,
dataTestSubj,
}: {
value: string;
onChange: (evt: ChangeEvent<HTMLInputElement>) => void;
label: string;
dataTestSubj: string;
}) => {
let point;
try {
point = convertMGRStoLL(value);
} catch (err) {
point = undefined;
}
const isInvalid =
value === '' ||
point === undefined ||
!point.north ||
isNaN(point.north) ||
!point.south ||
isNaN(point.south) ||
!point.east ||
isNaN(point.east) ||
!point.west ||
isNaN(point.west);
const error = isInvalid
? i18n.translate('xpack.maps.setViewControl.mgrsInvalid', {
defaultMessage: 'MGRS is invalid',
})
: null;
return {
isInvalid,
component: (
<EuiFormRow label={label} isInvalid={isInvalid} error={error} display="columnCompressed">
<EuiFieldText
compressed
value={value}
onChange={onChange}
isInvalid={isInvalid}
data-test-subj={dataTestSubj}
/>
</EuiFormRow>
),
};
};
_renderUTMZoneRow = ({
value,
onChange,
label,
dataTestSubj,
}: {
value: string | number;
onChange: (evt: ChangeEvent<HTMLInputElement>) => void;
label: string;
dataTestSubj: string;
}) => {
let point;
try {
point = converter.UTMtoLL(
this.state.utm.northing,
this.state.utm.easting,
this.state.utm.zoneNumber
);
} catch {
point = undefined;
}
const isInvalid = value === '' || point === undefined;
const error = isInvalid
? i18n.translate('xpack.maps.setViewControl.utmInvalidZone', {
defaultMessage: 'UTM Zone is invalid',
})
: null;
return {
isInvalid,
component: (
<EuiFormRow label={label} isInvalid={isInvalid} error={error} display="columnCompressed">
<EuiFieldText
compressed
value={value}
onChange={onChange}
isInvalid={isInvalid}
data-test-subj={dataTestSubj}
/>
</EuiFormRow>
),
};
};
_renderUTMEastingRow = ({
value,
onChange,
label,
dataTestSubj,
}: {
value: string | number;
onChange: (evt: ChangeEvent<HTMLInputElement>) => void;
label: string;
dataTestSubj: string;
}) => {
let point;
try {
point = converter.UTMtoLL(this.state.utm.northing, value, this.state.utm.zoneNumber);
} catch {
point = undefined;
}
const isInvalid = value === '' || point === undefined;
const error = isInvalid
? i18n.translate('xpack.maps.setViewControl.utmInvalidEasting', {
defaultMessage: 'UTM Easting is invalid',
})
: null;
return {
isInvalid,
component: (
<EuiFormRow label={label} isInvalid={isInvalid} error={error} display="columnCompressed">
<EuiFieldNumber
compressed
value={value}
onChange={onChange}
isInvalid={isInvalid}
data-test-subj={dataTestSubj}
/>
</EuiFormRow>
),
};
};
_renderUTMNorthingRow = ({
value,
onChange,
label,
dataTestSubj,
}: {
value: string | number;
onChange: (evt: ChangeEvent<HTMLInputElement>) => void;
label: string;
dataTestSubj: string;
}) => {
let point;
try {
point = converter.UTMtoLL(value, this.state.utm.easting, this.state.utm.zoneNumber);
} catch {
point = undefined;
}
const isInvalid = value === '' || point === undefined;
const error = isInvalid
? i18n.translate('xpack.maps.setViewControl.utmInvalidNorthing', {
defaultMessage: 'UTM Northing is invalid',
})
: null;
return {
isInvalid,
component: (
<EuiFormRow label={label} isInvalid={isInvalid} error={error} display="columnCompressed">
<EuiFieldNumber
compressed
value={value}
onChange={onChange}
isInvalid={isInvalid}
data-test-subj={dataTestSubj}
/>
</EuiFormRow>
),
};
};
_onSubmit = () => {
const { lat, lon, zoom } = this.state;
this._closePopover();
@ -124,27 +502,96 @@ export class SetViewControl extends Component<Props, State> {
};
_renderSetViewForm() {
const { isInvalid: isLatInvalid, component: latFormRow } = this._renderNumberFormRow({
value: this.state.lat,
min: -90,
max: 90,
onChange: this._onLatChange,
label: i18n.translate('xpack.maps.setViewControl.latitudeLabel', {
defaultMessage: 'Latitude',
}),
dataTestSubj: 'latitudeInput',
});
let isLatInvalid;
let latFormRow;
let isLonInvalid;
let lonFormRow;
let isMGRSInvalid;
let mgrsFormRow;
let isUtmZoneInvalid;
let utmZoneRow;
let isUtmEastingInvalid;
let utmEastingRow;
let isUtmNorthingInvalid;
let utmNorthingRow;
const { isInvalid: isLonInvalid, component: lonFormRow } = this._renderNumberFormRow({
value: this.state.lon,
min: -180,
max: 180,
onChange: this._onLonChange,
label: i18n.translate('xpack.maps.setViewControl.longitudeLabel', {
defaultMessage: 'Longitude',
}),
dataTestSubj: 'longitudeInput',
});
if (this.state.coord === COORDINATE_SYSTEM_DEGREES_DECIMAL) {
const latRenderObject = this._renderNumberFormRow({
value: this.state.lat,
min: -90,
max: 90,
onChange: this._onLatChange,
label: i18n.translate('xpack.maps.setViewControl.latitudeLabel', {
defaultMessage: 'Latitude',
}),
dataTestSubj: 'latitudeInput',
});
isLatInvalid = latRenderObject.isInvalid;
latFormRow = latRenderObject.component;
const lonRenderObject = this._renderNumberFormRow({
value: this.state.lon,
min: -180,
max: 180,
onChange: this._onLonChange,
label: i18n.translate('xpack.maps.setViewControl.longitudeLabel', {
defaultMessage: 'Longitude',
}),
dataTestSubj: 'longitudeInput',
});
isLonInvalid = lonRenderObject.isInvalid;
lonFormRow = lonRenderObject.component;
} else if (this.state.coord === COORDINATE_SYSTEM_MGRS) {
const mgrsRenderObject = this._renderMGRSFormRow({
value: this.state.mgrs,
onChange: this._onMGRSChange,
label: i18n.translate('xpack.maps.setViewControl.mgrsLabel', {
defaultMessage: 'MGRS',
}),
dataTestSubj: 'mgrsInput',
});
isMGRSInvalid = mgrsRenderObject.isInvalid;
mgrsFormRow = mgrsRenderObject.component;
} else if (this.state.coord === COORDINATE_SYSTEM_UTM) {
const utmZoneRenderObject = this._renderUTMZoneRow({
value: this.state.utm !== undefined ? this.state.utm.zone : '',
onChange: this._onUTMZoneChange,
label: i18n.translate('xpack.maps.setViewControl.utmZoneLabel', {
defaultMessage: 'UTM Zone',
}),
dataTestSubj: 'utmZoneInput',
});
isUtmZoneInvalid = utmZoneRenderObject.isInvalid;
utmZoneRow = utmZoneRenderObject.component;
const utmEastingRenderObject = this._renderUTMEastingRow({
value: this.state.utm !== undefined ? this.state.utm.easting : '',
onChange: this._onUTMEastingChange,
label: i18n.translate('xpack.maps.setViewControl.utmEastingLabel', {
defaultMessage: 'UTM Easting',
}),
dataTestSubj: 'utmEastingInput',
});
isUtmEastingInvalid = utmEastingRenderObject.isInvalid;
utmEastingRow = utmEastingRenderObject.component;
const utmNorthingRenderObject = this._renderUTMNorthingRow({
value: this.state.utm !== undefined ? this.state.utm.northing : '',
onChange: this._onUTMNorthingChange,
label: i18n.translate('xpack.maps.setViewControl.utmNorthingLabel', {
defaultMessage: 'UTM Northing',
}),
dataTestSubj: 'utmNorthingInput',
});
isUtmNorthingInvalid = utmNorthingRenderObject.isInvalid;
utmNorthingRow = utmNorthingRenderObject.component;
}
const { isInvalid: isZoomInvalid, component: zoomFormRow } = this._renderNumberFormRow({
value: this.state.zoom,
@ -157,13 +604,69 @@ export class SetViewControl extends Component<Props, State> {
dataTestSubj: 'zoomInput',
});
let coordinateInputs;
if (this.state.coord === 'dd') {
coordinateInputs = (
<Fragment>
{latFormRow}
{lonFormRow}
{zoomFormRow}
</Fragment>
);
} else if (this.state.coord === 'dms') {
coordinateInputs = (
<Fragment>
{latFormRow}
{lonFormRow}
{zoomFormRow}
</Fragment>
);
} else if (this.state.coord === 'utm') {
coordinateInputs = (
<Fragment>
{utmZoneRow}
{utmEastingRow}
{utmNorthingRow}
{zoomFormRow}
</Fragment>
);
} else if (this.state.coord === 'mgrs') {
coordinateInputs = (
<Fragment>
{mgrsFormRow}
{zoomFormRow}
</Fragment>
);
}
return (
<EuiForm data-test-subj="mapSetViewForm" style={{ width: 240 }}>
{latFormRow}
<EuiPopover
panelPaddingSize="s"
isOpen={this.state.isCoordPopoverOpen}
closePopover={() => {
this.setState({ isCoordPopoverOpen: false });
}}
button={
<EuiButtonEmpty
iconType="controlsHorizontal"
size="xs"
onClick={() => {
this.setState({ isCoordPopoverOpen: !this.state.isCoordPopoverOpen });
}}
>
Coordinate System
</EuiButtonEmpty>
}
>
<EuiRadioGroup
options={COORDINATE_SYSTEMS}
idSelected={this.state.coord}
onChange={this._onCoordinateSystemChange}
/>
</EuiPopover>
{lonFormRow}
{zoomFormRow}
{coordinateInputs}
<EuiSpacer size="s" />
@ -171,7 +674,15 @@ export class SetViewControl extends Component<Props, State> {
<EuiButton
size="s"
fill
disabled={isLatInvalid || isLonInvalid || isZoomInvalid}
disabled={
isLatInvalid ||
isLonInvalid ||
isZoomInvalid ||
isMGRSInvalid ||
isUtmZoneInvalid ||
isUtmEastingInvalid ||
isUtmNorthingInvalid
}
onClick={this._onSubmit}
data-test-subj="submitViewButton"
>
@ -215,3 +726,62 @@ export class SetViewControl extends Component<Props, State> {
);
}
}
function convertLatLonToUTM(lat: string | number, lon: string | number) {
const utmCoord = converter.LLtoUTM(lat, lon);
let eastwest = 'E';
if (utmCoord.easting < 0) {
eastwest = 'W';
}
let norwest = 'N';
if (utmCoord.northing < 0) {
norwest = 'S';
}
if (utmCoord !== 'undefined') {
utmCoord.zoneLetter = isNaN(lat) ? '' : converter.UTMLetterDesignator(lat);
utmCoord.zone = `${utmCoord.zoneNumber}${utmCoord.zoneLetter}`;
utmCoord.easting = Math.round(utmCoord.easting);
utmCoord.northing = Math.round(utmCoord.northing);
utmCoord.str = `${utmCoord.zoneNumber}${utmCoord.zoneLetter} ${utmCoord.easting}${eastwest} ${utmCoord.northing}${norwest}`;
}
return utmCoord;
}
function convertLatLonToMGRS(lat: string | number, lon: string | number) {
const mgrsCoord = converter.LLtoMGRS(lat, lon, 5);
return mgrsCoord;
}
function getViewString(lat: number, lon: number, zoom: number) {
return `${lat},${lon},${zoom}`;
}
function convertMGRStoUSNG(mgrs: string) {
let squareIdEastSpace = 0;
for (let i = mgrs.length - 1; i > -1; i--) {
// check if we have hit letters yet
if (isNaN(mgrs.substr(i, 1))) {
squareIdEastSpace = i + 1;
break;
}
}
const gridZoneSquareIdSpace = squareIdEastSpace ? squareIdEastSpace - 2 : -1;
const numPartLength = mgrs.substr(squareIdEastSpace).length / 2;
// add the number split space
const eastNorthSpace = squareIdEastSpace ? squareIdEastSpace + numPartLength : -1;
const stringArray = mgrs.split('');
stringArray.splice(eastNorthSpace, 0, ' ');
stringArray.splice(squareIdEastSpace, 0, ' ');
stringArray.splice(gridZoneSquareIdSpace, 0, ' ');
const rejoinedArray = stringArray.join('');
return rejoinedArray;
}
function convertMGRStoLL(mgrs: string) {
return mgrs ? converter.USNGtoLL(convertMGRStoUSNG(mgrs)) : '';
}

View file

@ -28047,6 +28047,11 @@ use@^2.0.0:
isobject "^3.0.0"
lazy-cache "^2.0.2"
usng.js@^0.4.5:
version "0.4.5"
resolved "https://registry.yarnpkg.com/usng.js/-/usng.js-0.4.5.tgz#49301e131baa9f7f7ab36c0539472c1aa1ecdae7"
integrity sha512-JTJcFFDy/JqA5iiU8DqMOV5gJiL3ZdXH0hCKYaRMDbbredhFna5Ok4C1YDFU07mTGAgm+5FzHaaOzQnO/3BzWQ==
utif@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/utif/-/utif-2.0.1.tgz#9e1582d9bbd20011a6588548ed3266298e711759"