mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Maps] fix Go To - lat/long values outside expected range cause blank Maps app (#141873)
* [Maps] fix Go To - lat/long values outside expected range cause blank Maps app * UTM form * wire UTM form * add unit tests * fix test names * fix expects * fix functional tests * review feedback
This commit is contained in:
parent
e4080d5b64
commit
cc9f1c6409
8 changed files with 804 additions and 719 deletions
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, Component } from 'react';
|
||||
import {
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiButton,
|
||||
EuiFieldNumber,
|
||||
EuiTextAlign,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { MapCenter, MapSettings } from '../../../../common/descriptor_types';
|
||||
import { withinRange } from './utils';
|
||||
|
||||
interface Props {
|
||||
settings: MapSettings;
|
||||
zoom: number;
|
||||
center: MapCenter;
|
||||
onSubmit: (lat: number, lon: number, zoom: number) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
lat: number | string;
|
||||
lon: number | string;
|
||||
zoom: number | string;
|
||||
}
|
||||
|
||||
export class DecimalDegreesForm extends Component<Props, State> {
|
||||
state: State = {
|
||||
lat: this.props.center.lat,
|
||||
lon: this.props.center.lon,
|
||||
zoom: this.props.zoom,
|
||||
};
|
||||
|
||||
_onLatChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const sanitizedValue = parseFloat(evt.target.value);
|
||||
this.setState({
|
||||
lat: isNaN(sanitizedValue) ? '' : sanitizedValue,
|
||||
});
|
||||
};
|
||||
|
||||
_onLonChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const sanitizedValue = parseFloat(evt.target.value);
|
||||
this.setState({
|
||||
lon: isNaN(sanitizedValue) ? '' : sanitizedValue,
|
||||
});
|
||||
};
|
||||
|
||||
_onZoomChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const sanitizedValue = parseFloat(evt.target.value);
|
||||
this.setState({
|
||||
zoom: isNaN(sanitizedValue) ? '' : sanitizedValue,
|
||||
});
|
||||
};
|
||||
|
||||
_onSubmit = () => {
|
||||
const { lat, lon, zoom } = this.state;
|
||||
this.props.onSubmit(lat as number, lon as number, zoom as number);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isInvalid: isLatInvalid, error: latError } = withinRange(this.state.lat, -90, 90);
|
||||
const { isInvalid: isLonInvalid, error: lonError } = withinRange(this.state.lon, -180, 180);
|
||||
const { isInvalid: isZoomInvalid, error: zoomError } = withinRange(
|
||||
this.state.zoom,
|
||||
this.props.settings.minZoom,
|
||||
this.props.settings.maxZoom
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.setViewControl.latitudeLabel', {
|
||||
defaultMessage: 'Latitude',
|
||||
})}
|
||||
isInvalid={isLatInvalid}
|
||||
error={latError}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
value={this.state.lat}
|
||||
onChange={this._onLatChange}
|
||||
isInvalid={isLatInvalid}
|
||||
data-test-subj="latitudeInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.setViewControl.longitudeLabel', {
|
||||
defaultMessage: 'Longitude',
|
||||
})}
|
||||
isInvalid={isLonInvalid}
|
||||
error={lonError}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
value={this.state.lon}
|
||||
onChange={this._onLonChange}
|
||||
isInvalid={isLonInvalid}
|
||||
data-test-subj="longitudeInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.setViewControl.zoomLabel', {
|
||||
defaultMessage: 'Zoom',
|
||||
})}
|
||||
isInvalid={isZoomInvalid}
|
||||
error={zoomError}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
value={this.state.zoom}
|
||||
onChange={this._onZoomChange}
|
||||
isInvalid={isZoomInvalid}
|
||||
data-test-subj="zoomInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiTextAlign textAlign="right">
|
||||
<EuiButton
|
||||
size="s"
|
||||
fill
|
||||
disabled={isLatInvalid || isLonInvalid || isZoomInvalid}
|
||||
onClick={this._onSubmit}
|
||||
data-test-subj="submitViewButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.setViewControl.submitButtonLabel"
|
||||
defaultMessage="Go"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiTextAlign>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { ChangeEvent, Component } from 'react';
|
||||
import {
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiButton,
|
||||
EuiFieldNumber,
|
||||
EuiFieldText,
|
||||
EuiTextAlign,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { MapCenter, MapSettings } from '../../../../common/descriptor_types';
|
||||
import { ddToMGRS, mgrsToDD, withinRange } from './utils';
|
||||
|
||||
interface Props {
|
||||
settings: MapSettings;
|
||||
zoom: number;
|
||||
center: MapCenter;
|
||||
onSubmit: (lat: number, lon: number, zoom: number) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
mgrs: string;
|
||||
zoom: number | string;
|
||||
}
|
||||
|
||||
export class MgrsForm extends Component<Props, State> {
|
||||
state: State = {
|
||||
mgrs: ddToMGRS(this.props.center.lat, this.props.center.lon),
|
||||
zoom: this.props.zoom,
|
||||
};
|
||||
|
||||
_toPoint() {
|
||||
return this.state.mgrs === '' ? undefined : mgrsToDD(this.state.mgrs);
|
||||
}
|
||||
|
||||
_isMgrsInvalid() {
|
||||
const point = this._toPoint();
|
||||
return (
|
||||
point === undefined ||
|
||||
!point.north ||
|
||||
_.isNaN(point.north) ||
|
||||
!point.south ||
|
||||
_.isNaN(point.south) ||
|
||||
!point.east ||
|
||||
_.isNaN(point.east) ||
|
||||
!point.west ||
|
||||
_.isNaN(point.west)
|
||||
);
|
||||
}
|
||||
|
||||
_onMGRSChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
mgrs: _.isNull(evt.target.value) ? '' : evt.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onZoomChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const sanitizedValue = parseFloat(evt.target.value);
|
||||
this.setState({
|
||||
zoom: isNaN(sanitizedValue) ? '' : sanitizedValue,
|
||||
});
|
||||
};
|
||||
|
||||
_onSubmit = () => {
|
||||
const point = this._toPoint();
|
||||
if (point) {
|
||||
this.props.onSubmit(point.north, point.east, this.state.zoom as number);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const isMgrsInvalid = this._isMgrsInvalid();
|
||||
const mgrsError = isMgrsInvalid
|
||||
? i18n.translate('xpack.maps.setViewControl.mgrsInvalid', {
|
||||
defaultMessage: 'MGRS is invalid',
|
||||
})
|
||||
: null;
|
||||
const { isInvalid: isZoomInvalid, error: zoomError } = withinRange(
|
||||
this.state.zoom,
|
||||
this.props.settings.minZoom,
|
||||
this.props.settings.maxZoom
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.setViewControl.mgrsLabel', {
|
||||
defaultMessage: 'MGRS',
|
||||
})}
|
||||
isInvalid={isMgrsInvalid}
|
||||
error={mgrsError}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed
|
||||
value={this.state.mgrs}
|
||||
onChange={this._onMGRSChange}
|
||||
isInvalid={isMgrsInvalid}
|
||||
data-test-subj="mgrsInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.setViewControl.zoomLabel', {
|
||||
defaultMessage: 'Zoom',
|
||||
})}
|
||||
isInvalid={isZoomInvalid}
|
||||
error={zoomError}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
value={this.state.zoom}
|
||||
onChange={this._onZoomChange}
|
||||
isInvalid={isZoomInvalid}
|
||||
data-test-subj="zoomInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiTextAlign textAlign="right">
|
||||
<EuiButton
|
||||
size="s"
|
||||
fill
|
||||
disabled={isMgrsInvalid || isZoomInvalid}
|
||||
onClick={this._onSubmit}
|
||||
data-test-subj="submitViewButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.setViewControl.submitButtonLabel"
|
||||
defaultMessage="Go"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiTextAlign>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
|
@ -5,50 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
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 React, { Component } from 'react';
|
||||
import { EuiButtonIcon, EuiPopover, EuiPanel } 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, MapSettings } from '../../../../common/descriptor_types';
|
||||
|
||||
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',
|
||||
},
|
||||
];
|
||||
import { SetViewForm } from './set_view_form';
|
||||
|
||||
export interface Props {
|
||||
settings: MapSettings;
|
||||
|
@ -59,73 +20,17 @@ export interface Props {
|
|||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
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> {
|
||||
state: State = {
|
||||
isPopoverOpen: false,
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
lat: this.props.center.lat,
|
||||
lon: this.props.center.lon,
|
||||
zoom: this.props.zoom,
|
||||
isPopoverOpen: true,
|
||||
});
|
||||
this.setState((prevState) => ({
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
_closePopover = () => {
|
||||
|
@ -134,567 +39,11 @@ export class SetViewControl extends Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
_onCoordinateSystemChange = (coordId: string) => {
|
||||
this.setState({
|
||||
coord: coordId,
|
||||
});
|
||||
};
|
||||
|
||||
_onLatChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
this._onChange('lat', evt);
|
||||
};
|
||||
|
||||
_onLonChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
this._onChange('lon', evt);
|
||||
};
|
||||
|
||||
_onZoomChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const sanitizedValue = parseFloat(evt.target.value);
|
||||
this.setState({
|
||||
['zoom']: isNaN(sanitizedValue) ? '' : sanitizedValue,
|
||||
});
|
||||
};
|
||||
|
||||
_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);
|
||||
|
||||
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 = ({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
onChange,
|
||||
label,
|
||||
dataTestSubj,
|
||||
}: {
|
||||
value: string | number;
|
||||
min: number;
|
||||
max: number;
|
||||
onChange: (evt: ChangeEvent<HTMLInputElement>) => void;
|
||||
label: string;
|
||||
dataTestSubj: string;
|
||||
}) => {
|
||||
const isInvalid = value === '' || value > max || value < min;
|
||||
const error = isInvalid ? `Must be between ${min} and ${max}` : 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>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
_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;
|
||||
_onSubmit = (lat: number, lon: number, zoom: number) => {
|
||||
this._closePopover();
|
||||
this.props.onSubmit({ lat: lat as number, lon: lon as number, zoom: zoom as number });
|
||||
this.props.onSubmit({ lat, lon, zoom });
|
||||
};
|
||||
|
||||
_renderSetViewForm() {
|
||||
let isLatInvalid;
|
||||
let latFormRow;
|
||||
let isLonInvalid;
|
||||
let lonFormRow;
|
||||
let isMGRSInvalid;
|
||||
let mgrsFormRow;
|
||||
let isUtmZoneInvalid;
|
||||
let utmZoneRow;
|
||||
let isUtmEastingInvalid;
|
||||
let utmEastingRow;
|
||||
let isUtmNorthingInvalid;
|
||||
let utmNorthingRow;
|
||||
|
||||
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,
|
||||
min: this.props.settings.minZoom,
|
||||
max: this.props.settings.maxZoom,
|
||||
onChange: this._onZoomChange,
|
||||
label: i18n.translate('xpack.maps.setViewControl.zoomLabel', {
|
||||
defaultMessage: 'Zoom',
|
||||
}),
|
||||
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 }}>
|
||||
<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>
|
||||
|
||||
{coordinateInputs}
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiTextAlign textAlign="right">
|
||||
<EuiButton
|
||||
size="s"
|
||||
fill
|
||||
disabled={
|
||||
isLatInvalid ||
|
||||
isLonInvalid ||
|
||||
isZoomInvalid ||
|
||||
isMGRSInvalid ||
|
||||
isUtmZoneInvalid ||
|
||||
isUtmEastingInvalid ||
|
||||
isUtmNorthingInvalid
|
||||
}
|
||||
onClick={this._onSubmit}
|
||||
data-test-subj="submitViewButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.setViewControl.submitButtonLabel"
|
||||
defaultMessage="Go"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiTextAlign>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPopover
|
||||
|
@ -720,67 +69,13 @@ export class SetViewControl extends Component<Props, State> {
|
|||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this._closePopover}
|
||||
>
|
||||
{this._renderSetViewForm()}
|
||||
<SetViewForm
|
||||
settings={this.props.settings}
|
||||
zoom={this.props.zoom}
|
||||
center={this.props.center}
|
||||
onSubmit={this._onSubmit}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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)) : '';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { EuiButtonEmpty, EuiPopover, EuiRadioGroup } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { MapCenter, MapSettings } from '../../../../common/descriptor_types';
|
||||
import { DecimalDegreesForm } from './decimal_degrees_form';
|
||||
import { MgrsForm } from './mgrs_form';
|
||||
import { UtmForm } from './utm_form';
|
||||
|
||||
const DEGREES_DECIMAL = 'dd';
|
||||
const MGRS = 'mgrs';
|
||||
const UTM = 'utm';
|
||||
|
||||
const COORDINATE_SYSTEM_OPTIONS = [
|
||||
{
|
||||
id: DEGREES_DECIMAL,
|
||||
label: i18n.translate('xpack.maps.setViewControl.decimalDegreesLabel', {
|
||||
defaultMessage: 'Decimal degrees',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: UTM,
|
||||
label: 'UTM',
|
||||
},
|
||||
{
|
||||
id: MGRS,
|
||||
label: 'MGRS',
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
settings: MapSettings;
|
||||
zoom: number;
|
||||
center: MapCenter;
|
||||
onSubmit: (lat: number, lon: number, zoom: number) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
coordinateSystem: string;
|
||||
}
|
||||
|
||||
export class SetViewForm extends Component<Props, State> {
|
||||
state: State = {
|
||||
coordinateSystem: DEGREES_DECIMAL,
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
|
||||
_togglePopover = () => {
|
||||
this.setState((prevState) => ({
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
_closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
_onCoordinateSystemChange = (optionId: string) => {
|
||||
this._closePopover();
|
||||
this.setState({
|
||||
coordinateSystem: optionId,
|
||||
});
|
||||
};
|
||||
|
||||
_renderForm() {
|
||||
if (this.state.coordinateSystem === MGRS) {
|
||||
return (
|
||||
<MgrsForm
|
||||
settings={this.props.settings}
|
||||
zoom={this.props.zoom}
|
||||
center={this.props.center}
|
||||
onSubmit={this.props.onSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.coordinateSystem === UTM) {
|
||||
return (
|
||||
<UtmForm
|
||||
settings={this.props.settings}
|
||||
zoom={this.props.zoom}
|
||||
center={this.props.center}
|
||||
onSubmit={this.props.onSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DecimalDegreesForm
|
||||
settings={this.props.settings}
|
||||
zoom={this.props.zoom}
|
||||
center={this.props.center}
|
||||
onSubmit={this.props.onSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div data-test-subj="mapSetViewForm" style={{ width: 240 }}>
|
||||
<EuiPopover
|
||||
panelPaddingSize="s"
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this._closePopover}
|
||||
button={
|
||||
<EuiButtonEmpty iconType="controlsHorizontal" size="xs" onClick={this._togglePopover}>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.setViewControl.changeCoordinateSystemButtonLabel"
|
||||
defaultMessage="Coordinate system"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
>
|
||||
<EuiRadioGroup
|
||||
options={COORDINATE_SYSTEM_OPTIONS}
|
||||
idSelected={this.state.coordinateSystem}
|
||||
onChange={this._onCoordinateSystemChange}
|
||||
/>
|
||||
</EuiPopover>
|
||||
{this._renderForm()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ddToMGRS, mgrsToDD, ddToUTM, utmToDD } from './utils';
|
||||
|
||||
describe('MGRS', () => {
|
||||
test('ddToMGRS should convert lat lon to MGRS', () => {
|
||||
expect(ddToMGRS(29.29926, 32.05495)).toEqual('36RVT08214151');
|
||||
});
|
||||
|
||||
test('ddToMGRS should return empty string for lat lon that does not translate to MGRS grid', () => {
|
||||
expect(ddToMGRS(90, 32.05495)).toEqual('');
|
||||
});
|
||||
|
||||
test('mgrsToDD should convert MGRS to lat lon', () => {
|
||||
expect(mgrsToDD('36RVT08214151')).toEqual({
|
||||
east: 32.05498649594143,
|
||||
north: 29.299330195900975,
|
||||
south: 29.299239224067065,
|
||||
west: 32.054884373627345,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('UTM', () => {
|
||||
test('ddToUTM should convert lat lon to UTM', () => {
|
||||
expect(ddToUTM(29.29926, 32.05495)).toEqual({
|
||||
easting: '408216',
|
||||
northing: '3241512',
|
||||
zone: '36R',
|
||||
});
|
||||
});
|
||||
|
||||
test('ddToUTM should return empty strings for lat lon that does not translate to UTM grid', () => {
|
||||
expect(ddToUTM(90, 32.05495)).toEqual({
|
||||
northing: '',
|
||||
easting: '',
|
||||
zone: '',
|
||||
});
|
||||
});
|
||||
|
||||
test('utmToDD should convert UTM to lat lon', () => {
|
||||
expect(utmToDD('3241512', '408216', '36R')).toEqual({
|
||||
lat: 29.29925770984472,
|
||||
lon: 32.05494597943409,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import * as usng from 'usng.js';
|
||||
|
||||
// @ts-ignore
|
||||
const converter = new usng.Converter();
|
||||
|
||||
export function withinRange(value: string | number, min: number, max: number) {
|
||||
const isInvalid = value === '' || value > max || value < min;
|
||||
const error = isInvalid
|
||||
? i18n.translate('xpack.maps.setViewControl.outOfRangeErrorMsg', {
|
||||
defaultMessage: `Must be between {min} and {max}`,
|
||||
values: { min, max },
|
||||
})
|
||||
: null;
|
||||
return { isInvalid, error };
|
||||
}
|
||||
|
||||
export function ddToUTM(lat: number, lon: number) {
|
||||
try {
|
||||
const utm = converter.LLtoUTM(lat, lon);
|
||||
return {
|
||||
northing: utm === converter.UNDEFINED_STR ? '' : String(Math.round(utm.northing)),
|
||||
easting: utm === converter.UNDEFINED_STR ? '' : String(Math.round(utm.easting)),
|
||||
zone:
|
||||
utm === converter.UNDEFINED_STR
|
||||
? ''
|
||||
: `${utm.zoneNumber}${converter.UTMLetterDesignator(lat)}`,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
northing: '',
|
||||
easting: '',
|
||||
zone: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function utmToDD(northing: string, easting: string, zoneNumber: string) {
|
||||
try {
|
||||
return converter.UTMtoLL(northing, easting, zoneNumber);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function ddToMGRS(lat: number, lon: number) {
|
||||
try {
|
||||
const mgrsCoord = converter.LLtoMGRS(lat, lon, 5);
|
||||
return mgrsCoord;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function mgrstoUSNG(mgrs: string) {
|
||||
let squareIdEastSpace = 0;
|
||||
for (let i = mgrs.length - 1; i > -1; i--) {
|
||||
// check if we have hit letters yet
|
||||
if (isNaN(parseInt(mgrs.substr(i, 1), 10))) {
|
||||
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;
|
||||
}
|
||||
|
||||
export function mgrsToDD(mgrs: string) {
|
||||
try {
|
||||
return converter.USNGtoLL(mgrstoUSNG(mgrs));
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { ChangeEvent, Component } from 'react';
|
||||
import {
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiButton,
|
||||
EuiFieldNumber,
|
||||
EuiFieldText,
|
||||
EuiTextAlign,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { MapCenter, MapSettings } from '../../../../common/descriptor_types';
|
||||
import { ddToUTM, utmToDD, withinRange } from './utils';
|
||||
|
||||
interface Props {
|
||||
settings: MapSettings;
|
||||
zoom: number;
|
||||
center: MapCenter;
|
||||
onSubmit: (lat: number, lon: number, zoom: number) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
northing: string;
|
||||
easting: string;
|
||||
zone: string;
|
||||
zoom: number | string;
|
||||
}
|
||||
|
||||
export class UtmForm extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const utm = ddToUTM(this.props.center.lat, this.props.center.lon);
|
||||
this.state = {
|
||||
northing: utm.northing,
|
||||
easting: utm.easting,
|
||||
zone: utm.zone,
|
||||
zoom: this.props.zoom,
|
||||
};
|
||||
}
|
||||
|
||||
_toPoint() {
|
||||
const { northing, easting, zone } = this.state;
|
||||
return northing === '' || easting === '' || zone.length < 2
|
||||
? undefined
|
||||
: utmToDD(northing, easting, zone.substring(0, zone.length - 1));
|
||||
}
|
||||
|
||||
_isUtmInvalid() {
|
||||
const point = this._toPoint();
|
||||
return point === undefined;
|
||||
}
|
||||
|
||||
_onZoneChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
zone: _.isNull(evt.target.value) ? '' : evt.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onEastingChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
easting: _.isNull(evt.target.value) ? '' : evt.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onNorthingChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
northing: _.isNull(evt.target.value) ? '' : evt.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_onZoomChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const sanitizedValue = parseFloat(evt.target.value);
|
||||
this.setState({
|
||||
zoom: isNaN(sanitizedValue) ? '' : sanitizedValue,
|
||||
});
|
||||
};
|
||||
|
||||
_onSubmit = () => {
|
||||
const point = this._toPoint();
|
||||
if (point) {
|
||||
this.props.onSubmit(point.lat, point.lon, this.state.zoom as number);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const isUtmInvalid = this._isUtmInvalid();
|
||||
const northingError =
|
||||
isUtmInvalid || this.state.northing === ''
|
||||
? i18n.translate('xpack.maps.setViewControl.utmInvalidNorthing', {
|
||||
defaultMessage: 'UTM Northing is invalid',
|
||||
})
|
||||
: null;
|
||||
const eastingError =
|
||||
isUtmInvalid || this.state.northing === ''
|
||||
? i18n.translate('xpack.maps.setViewControl.utmInvalidEasting', {
|
||||
defaultMessage: 'UTM Easting is invalid',
|
||||
})
|
||||
: null;
|
||||
const zoneError =
|
||||
isUtmInvalid || this.state.northing === ''
|
||||
? i18n.translate('xpack.maps.setViewControl.utmInvalidZone', {
|
||||
defaultMessage: 'UTM Zone is invalid',
|
||||
})
|
||||
: null;
|
||||
const { isInvalid: isZoomInvalid, error: zoomError } = withinRange(
|
||||
this.state.zoom,
|
||||
this.props.settings.minZoom,
|
||||
this.props.settings.maxZoom
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.setViewControl.utmZoneLabel', {
|
||||
defaultMessage: 'UTM Zone',
|
||||
})}
|
||||
isInvalid={isUtmInvalid}
|
||||
error={zoneError}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed
|
||||
value={this.state.zone}
|
||||
onChange={this._onZoneChange}
|
||||
isInvalid={isUtmInvalid}
|
||||
data-test-subj="utmZoneInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.setViewControl.utmEastingLabel', {
|
||||
defaultMessage: 'UTM Easting',
|
||||
})}
|
||||
isInvalid={isUtmInvalid}
|
||||
error={eastingError}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
value={this.state.easting}
|
||||
onChange={this._onEastingChange}
|
||||
isInvalid={isUtmInvalid}
|
||||
data-test-subj="utmEastingInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.setViewControl.utmNorthingLabel', {
|
||||
defaultMessage: 'UTM Northing',
|
||||
})}
|
||||
isInvalid={isUtmInvalid}
|
||||
error={northingError}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
value={this.state.northing}
|
||||
onChange={this._onNorthingChange}
|
||||
isInvalid={isUtmInvalid}
|
||||
data-test-subj="utmNorthingInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.setViewControl.zoomLabel', {
|
||||
defaultMessage: 'Zoom',
|
||||
})}
|
||||
isInvalid={isZoomInvalid}
|
||||
error={zoomError}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
value={this.state.zoom}
|
||||
onChange={this._onZoomChange}
|
||||
isInvalid={isZoomInvalid}
|
||||
data-test-subj="zoomInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiTextAlign textAlign="right">
|
||||
<EuiButton
|
||||
size="s"
|
||||
fill
|
||||
disabled={isUtmInvalid || isZoomInvalid}
|
||||
onClick={this._onSubmit}
|
||||
data-test-subj="submitViewButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.setViewControl.submitButtonLabel"
|
||||
defaultMessage="Go"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiTextAlign>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue