Skip to content

Commit

Permalink
Merge pull request #3062 from NCI-Agency/GH-3042-add-mgrs-coordinate-…
Browse files Browse the repository at this point in the history
…format

GH 3042 Add MGRS coordinate format
  • Loading branch information
VassilIordanov authored Sep 8, 2020
2 parents cbd940a + ed28f0a commit 715fe3a
Show file tree
Hide file tree
Showing 8 changed files with 443 additions and 86 deletions.
3 changes: 3 additions & 0 deletions anet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,9 @@ dictionary:
helpText: Help text for object date field
visibleWhen: $[?(@.colourOptions === 'GREEN')]

location:
format: LAT_LON

position:
name: 'Position Name'

Expand Down
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
"locale-compare-polyfill": "0.0.2",
"lodash": "4.17.20",
"mathjs": "7.2.0",
"mgrs": "1.0.0",
"milsymbol": "2.0.0",
"ml-matrix": "6.5.1",
"moment": "2.27.0",
Expand Down
54 changes: 54 additions & 0 deletions client/src/geoUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { forward, toPoint } from "mgrs"

export function parseCoordinate(latLng) {
const value = parseFloat(latLng)
if (!value && value !== 0) {
return null
}

/*
* We use 5 decimal point (~110cm) precision because MGRS has
* a minimum of 1 meter precision.
* Please see;
* https://stackoverflow.com/a/16743805/1209097
* https://en.wikipedia.org/wiki/Military_Grid_Reference_System
*/
const precision = 5
/*
* for the purpose of rounding below please see:
* https://stackoverflow.com/questions/1458633/how-to-deal-with-floating-point-number-precision-in-javascript
* https://floating-point-gui.de/
*/
const safeRoundedValue = Math.round(value * 10 ** precision * 10) / 10
/*
* Also, coordinates are truncated instead of rounding when changing
* precision level in order to aviod inconsistencies during (MGRS <--> Lat/Lon) conversion.
*/
return Math.trunc(safeRoundedValue) / 10 ** precision
}

export function convertLatLngToMGRS(lat, lng) {
const parsedLat = parseCoordinate(lat)
const parsedLng = parseCoordinate(lng)

let mgrs = ""
try {
if ((parsedLat || parsedLat === 0) && (parsedLng || parsedLng === 0)) {
mgrs = forward([parsedLng, parsedLat])
}
} catch (e) {
mgrs = ""
}
return mgrs
}

export function convertMGRSToLatLng(mgrs) {
let latLng
try {
// toPoint returns an array of [lon, lat]
latLng = mgrs ? toPoint(mgrs) : ["", ""]
} catch (e) {
latLng = ["", ""]
}
return [parseCoordinate(latLng[1]), parseCoordinate(latLng[0])]
}
35 changes: 26 additions & 9 deletions client/src/models/Location.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Model from "components/Model"
import { convertMGRSToLatLng } from "geoUtils"
import _isEmpty from "lodash/isEmpty"
import LOCATIONS_ICON from "resources/locations.png"
import Settings from "settings"
import utils from "utils"
import * as yup from "yup"

Expand Down Expand Up @@ -53,6 +56,29 @@ export default class Location extends Model {
return true
})
.default(null),
// not actually in the database, but used for validation
displayedCoordinate: yup
.string()
.nullable()
.test({
name: "displayedCoordinate",
test: function(displayedCoordinate) {
if (_isEmpty(displayedCoordinate)) {
return true
}
if (Settings?.fields?.location?.format === "MGRS") {
const latLngValue = convertMGRSToLatLng(displayedCoordinate)
return !latLngValue[0] || !latLngValue[1]
? this.createError({
message: "Please enter a valid MGRS coordinate",
path: "displayedCoordinate"
})
: true
}
return true
}
})
.default(null),
// FIXME: resolve code duplication in yup schema for approval steps
planningApprovalSteps: yup
.array()
Expand Down Expand Up @@ -99,15 +125,6 @@ export default class Location extends Model {

static autocompleteQuery = "uuid, name"

static parseCoordinate(latLng) {
const value = parseFloat(latLng)
if (!value && value !== 0) {
return null
}
// 6 decimal point (~10cm) precision https://stackoverflow.com/a/16743805/1209097
return parseFloat(value.toFixed(6))
}

static hasCoordinates(location) {
return (
location &&
Expand Down
44 changes: 24 additions & 20 deletions client/src/pages/locations/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Messages from "components/Messages"
import NavigationWarning from "components/NavigationWarning"
import { jumpToTop } from "components/Page"
import { FastField, Form, Formik } from "formik"
import { parseCoordinate } from "geoUtils"
import _escape from "lodash/escape"
import { Location, Position } from "models"
import PropTypes from "prop-types"
Expand Down Expand Up @@ -80,12 +81,11 @@ const LocationForm = ({ edit, title, initialValues }) => {
initialValues={initialValues}
>
{({
handleSubmit,
isSubmitting,
dirty,
errors,
setFieldTouched,
setFieldValue,
setValues,
values,
submitForm
}) => {
Expand All @@ -94,7 +94,14 @@ const LocationForm = ({ edit, title, initialValues }) => {
name: _escape(values.name) || "", // escape HTML in location name!
draggable: true,
autoPan: true,
onMove: (event, map) => onMarkerMove(event, map, setFieldValue)
onMove: (event, map) => {
const latLng = map.wrapLatLng(event.target.getLatLng())
setValues({
...values,
lat: parseCoordinate(latLng.lat),
lng: parseCoordinate(latLng.lng)
})
}
}
if (Location.hasCoordinates(values)) {
Object.assign(marker, {
Expand Down Expand Up @@ -140,7 +147,7 @@ const LocationForm = ({ edit, title, initialValues }) => {
lat={values.lat}
lng={values.lng}
isSubmitting={isSubmitting}
setFieldValue={setFieldValue}
setValues={vals => setValues({ ...values, ...vals })}
setFieldTouched={setFieldTouched}
/>
</Fieldset>
Expand All @@ -149,7 +156,12 @@ const LocationForm = ({ edit, title, initialValues }) => {
<Leaflet
markers={[marker]}
onMapClick={(event, map) => {
onMarkerMapClick(event, map, setFieldValue)
const latLng = map.wrapLatLng(event.latlng)
setValues({
...values,
lat: parseCoordinate(latLng.lat),
lng: parseCoordinate(latLng.lng)
})
}}
/>

Expand Down Expand Up @@ -196,24 +208,12 @@ const LocationForm = ({ edit, title, initialValues }) => {
</Formik>
)

function onMarkerMove(event, map, setFieldValue) {
const latLng = map.wrapLatLng(event.target.getLatLng())
setFieldValue("lat", Location.parseCoordinate(latLng.lat))
setFieldValue("lng", Location.parseCoordinate(latLng.lng))
}

function onMarkerMapClick(event, map, setFieldValue) {
const latLng = map.wrapLatLng(event.latlng)
setFieldValue("lat", Location.parseCoordinate(latLng.lat))
setFieldValue("lng", Location.parseCoordinate(latLng.lng))
}

function onCancel() {
history.goBack()
}

function onSubmit(values, form) {
return save(values, form)
return save(values)
.then(response => onSubmitSuccess(response, values, form))
.catch(error => {
setError(error)
Expand All @@ -240,8 +240,12 @@ const LocationForm = ({ edit, title, initialValues }) => {
})
}

function save(values, form) {
const location = Object.without(new Location(values), "notes")
function save(values) {
const location = Object.without(
new Location(values),
"notes",
"displayedCoordinate"
)
return API.mutation(edit ? GQL_UPDATE_LOCATION : GQL_CREATE_LOCATION, {
location
})
Expand Down
Loading

0 comments on commit 715fe3a

Please sign in to comment.