diff --git a/web/client/components/map/leaflet/MeasurementSupport.jsx b/web/client/components/map/leaflet/MeasurementSupport.jsx index ab836f9a8d..27bf7a469b 100644 --- a/web/client/components/map/leaflet/MeasurementSupport.jsx +++ b/web/client/components/map/leaflet/MeasurementSupport.jsx @@ -2,10 +2,144 @@ const PropTypes = require('prop-types'); const React = require('react'); const assign = require('object-assign'); var L = require('leaflet'); -const {slice} = require('lodash'); -const {reproject, calculateAzimuth, calculateDistance, transformLineToArcs} = require('../../../utils/CoordinatesUtils'); +const { + slice +} = require('lodash'); +const { + reproject, + calculateAzimuth, + calculateDistance, + transformLineToArcs +} = require('../../../utils/CoordinatesUtils'); +const { + convertUom, + getFormattedBearingValue +} = require('../../../utils/MeasureUtils'); require('leaflet-draw'); +let defaultPrecision = { + km: 2, + ha: 2, + m: 2, + mi: 2, + ac: 2, + yd: 0, + ft: 0, + nm: 2, + sqkm: 2, + sqha: 2, + sqm: 2, + sqmi: 2, + sqac: 2, + sqyd: 2, + sqft: 2, + sqnm: 2 +}; + + +L.getMeasureWithTreshold = (value, threshold, source, dest, precision, sourceLabel, destLabel) => { + if (value > threshold) { + return L.GeometryUtil.formattedNumber(convertUom(value, source, dest), precision[dest]) + ' ' + destLabel; + } + return L.GeometryUtil.formattedNumber(value, precision[source]) + ' ' + sourceLabel; +}; + +/** @method readableDistance(distance, units): string + * Converts a metric distance to one of [ feet, nauticalMile, metric or yards ] string + * + * @alternative + * @method readableDistance(distance, isMetric, useFeet, isNauticalMile, precision, options): string + * Converts metric distance to distance string. + * The value will be rounded as defined by the precision option object. + * this override is necesary due to the customization on how the measure label is presented and for adding bearing support +*/ + +L.GeometryUtil.readableDistance = (distance, isMetric, isFeet, isNauticalMile, precision, options) => { + if (options.geomType === 'Bearing') { + return options.bearing; + } + let p = L.Util.extend({}, defaultPrecision, precision); + const {unit, label} = options.uom.length; + + let distanceStr = L.GeometryUtil.formattedNumber(convertUom(distance, "m", unit), p[unit]) + ' ' + label; + if (options.useTreshold) { + if (isMetric) { + distanceStr = L.getMeasureWithTreshold(distance, 1000, "m", "km", p, "m", "km"); + } + if (unit === "mi") { + distanceStr = L.getMeasureWithTreshold(convertUom(distance, "m", "yd"), 1760, "yd", "mi", p, "yd", "mi"); + } + } + return distanceStr; +}; + +/** @method readableArea(area, isMetric, precision): string + * @return a readable area string in yards or metric. + * The value will be rounded as defined by the precision option object. + * this override is necesary due to the customization on how the area measure label is presented + supporting also the square nautical miles and square feets + */ +L.GeometryUtil.readableArea = (area, isMetric, precision, options) => { + const {unit, label} = options.uom.area; + let p = L.Util.extend({}, defaultPrecision, precision); + let areaStr = L.GeometryUtil.formattedNumber(convertUom(area, "sqm", unit), p[unit]) + ' ' + label; + if (options.useTreshold) { // this is done for retrocompatibility + if (isMetric) { + areaStr = L.getMeasureWithTreshold(area, 1000000, "sqm", "sqkm", p, "m²", "km²"); + } + if (unit === "sqmi") { + areaStr = L.getMeasureWithTreshold(convertUom(area, "sqm", "sqyd"), 3097600, "sqyd", "sqmi", p, "yd²", "mi²"); + } + } + return areaStr; +}; + +/** + * this is need to pass custom options as uom, useTreshold to the readableArea function +*/ +L.Draw.Polygon.prototype._getMeasurementString = function() { + let area = this._area; + let measurementString = ''; + if (!area && !this.options.showLength) { + return null; + } + if (this.options.showLength) { + measurementString = L.Draw.Polyline.prototype._getMeasurementString.call(this); + } + + if (area) { + // here is the custom option passed to geom util func + const opt = { + uom: this.options.uom, + useTreshold: this.options.useTreshold + }; + measurementString += this.options.showLength ? '<br>' : '' + L.GeometryUtil.readableArea(area, this.options.metric, this.options.precision, opt); + } + return measurementString; +}; +/** + * this is need to pass custom options as uom, useTreshold, bearing to the readableDistance function +*/ +L.Draw.Polyline.prototype._getMeasurementString = function() { + let currentLatLng = this._currentLatLng; + let previousLatLng = this._markers[this._markers.length - 1].getLatLng(); + let distance; + + // Calculate the distance from the last fixed point to the mouse position based on the version + if (L.GeometryUtil.isVersion07x()) { + distance = previousLatLng && currentLatLng && currentLatLng.distanceTo ? this._measurementRunningTotal + currentLatLng.distanceTo(previousLatLng) * (this.options.factor || 1) : this._measurementRunningTotal || 0; + } else { + distance = previousLatLng && currentLatLng ? this._measurementRunningTotal + this._map.distance(currentLatLng, previousLatLng) * (this.options.factor || 1) : this._measurementRunningTotal || 0; + } + // here is the custom option passed to geom util func + const opt = { + uom: this.options.uom, + useTreshold: this.options.useTreshold, + geomType: this.options.geomType, + bearing: this.options.bearing ? getFormattedBearingValue(this.options.bearing) : 0 + }; + return L.GeometryUtil.readableDistance(distance, this.options.metric, this.options.feet, this.options.nautic, this.options.precision, opt); +}; class MeasurementSupport extends React.Component { static displayName = 'MeasurementSupport'; @@ -13,10 +147,13 @@ class MeasurementSupport extends React.Component { map: PropTypes.object, metric: PropTypes.bool, feet: PropTypes.bool, + nautic: PropTypes.bool, + useTreshold: PropTypes.bool, projection: PropTypes.string, measurement: PropTypes.object, changeMeasurementState: PropTypes.func, messages: PropTypes.object, + uom: PropTypes.object, updateOnMouseMove: PropTypes.bool }; @@ -25,20 +162,34 @@ class MeasurementSupport extends React.Component { }; static defaultProps = { + uom: { + length: {unit: 'm', label: 'm'}, + area: {unit: 'sqm', label: 'm²'} + }, updateOnMouseMove: false, metric: true, + nautic: false, + useTreshold: false, feet: false }; + componentWillReceiveProps(newProps) { - if (newProps.measurement.geomType && newProps.measurement.geomType !== this.props.measurement.geomType ) { + if ((newProps && newProps.uom && newProps.uom.length && newProps.uom.length.unit) !== (this.props && this.props.uom && this.props.uom.length && this.props.uom.length.unit) && this.drawControl) { + const uomOptions = this.uomLengthOptions(newProps); + this.drawControl.setOptions({...uomOptions, uom: newProps.uom}); + } + if ((newProps && newProps.uom && newProps.uom.area && newProps.uom.area.unit) !== (this.props && this.props.uom && this.props.uom.area && this.props.uom.area.unit) && this.drawControl) { + const uomOptions = this.uomAreaOptions(newProps); + this.drawControl.setOptions({...uomOptions, uom: newProps.uom}); + } + if (newProps.measurement.geomType && newProps.measurement.geomType !== this.props.measurement.geomType) { this.addDrawInteraction(newProps); } if (!newProps.measurement.geomType) { this.removeDrawInteraction(); } } - onDrawStart = () => { this.removeArcLayer(); this.drawing = true; @@ -54,11 +205,20 @@ class MeasurementSupport extends React.Component { let feature = this.lastLayer && this.lastLayer.toGeoJSON() || {}; if (this.props.measurement.geomType === 'Point') { let pos = this.drawControl._marker.getLatLng(); - let point = {x: pos.lng, y: pos.lat, srs: 'EPSG:4326'}; - let newMeasureState = assign({}, this.props.measurement, {point: point, feature}); + let point = { + x: pos.lng, + y: pos.lat, + srs: 'EPSG:4326' + }; + let newMeasureState = assign({}, this.props.measurement, { + point: point, + feature + }); this.props.changeMeasurementState(newMeasureState); } else { - let newMeasureState = assign({}, this.props.measurement, {feature}); + let newMeasureState = assign({}, this.props.measurement, { + feature + }); this.props.changeMeasurementState(newMeasureState); } if (this.props.measurement.lineMeasureEnabled && this.lastLayer) { @@ -114,7 +274,7 @@ class MeasurementSupport extends React.Component { if (this.props.measurement.geomType === 'LineString' && this.drawControl._markers && this.drawControl._markers.length > 1) { // calculate length const reprojectedCoords = this.drawControl._markers.reduce((p, c) => { - const {lng, lat} = c.getLatLng(); + const { lng, lat } = c.getLatLng(); return [...p, [lng, lat]]; }, []); @@ -126,29 +286,15 @@ class MeasurementSupport extends React.Component { area = L.GeometryUtil.geodesicArea(latLngs); } else if (this.props.measurement.geomType === 'Bearing' && this.drawControl._markers && this.drawControl._markers.length > 0) { // calculate bearing - let bearingMarkers = this.drawControl._markers; - let coords1 = [bearingMarkers[0].getLatLng().lng, bearingMarkers[0].getLatLng().lat]; - let coords2; - if (bearingMarkers.length === 1) { - coords2 = [currentLatLng.lng, currentLatLng.lat]; - } else if (bearingMarkers.length === 2) { - coords2 = [bearingMarkers[1].getLatLng().lng, bearingMarkers[1].getLatLng().lat]; - } - // in order to align the results between leaflet and openlayers the coords are repojected only for leaflet - coords1 = reproject(coords1, 'EPSG:4326', this.props.projection); - coords2 = reproject(coords2, 'EPSG:4326', this.props.projection); - // calculate the azimuth as base for bearing information - bearing = calculateAzimuth(coords1, coords2, this.props.projection); + bearing = this.calculateBearing(); } // let drawn geom stay on the map - let newMeasureState = assign({}, this.props.measurement, - { - point: null, // Point is set in onDraw.created - len: distance, - area: area, - bearing: bearing - } - ); + let newMeasureState = assign({}, this.props.measurement, { + point: null, // Point is set in onDraw.created + len: distance, + area: area, + bearing: bearing + }); this.props.changeMeasurementState(newMeasureState); }; @@ -182,23 +328,28 @@ class MeasurementSupport extends React.Component { this.props.map.on('draw:created', this.onDrawCreated, this); this.props.map.on('draw:drawstart', this.onDrawStart, this); this.props.map.on('click', this.mapClickHandler, this); + this.props.map.on('mousemove', this.updateBearing, this); + if (this.props.updateOnMouseMove) { this.props.map.on('mousemove', this.updateMeasurementResults, this); } - if (newProps.measurement.geomType === 'Point') { this.drawControl = new L.Draw.Marker(this.props.map, { repeatMode: false }); } else if (newProps.measurement.geomType === 'LineString' || - newProps.measurement.geomType === 'Bearing') { + newProps.measurement.geomType === 'Bearing') { + const uomOptions = this.uomLengthOptions(newProps); this.drawControl = new L.Draw.Polyline(this.props.map, { shapeOptions: { color: '#ffcc33', weight: 2 }, - metric: this.props.metric, - feet: this.props.feet, + showLength: true, + useTreshold: newProps.useTreshold, + uom: newProps.uom, + geomType: newProps.measurement.geomType, + ...uomOptions, repeatMode: false, icon: new L.DivIcon({ iconSize: new L.Point(8, 8), @@ -210,13 +361,21 @@ class MeasurementSupport extends React.Component { }) }); } else if (newProps.measurement.geomType === 'Polygon') { + const uomOptions = this.uomAreaOptions(newProps); this.drawControl = new L.Draw.Polygon(this.props.map, { shapeOptions: { color: '#ffcc33', weight: 2, fill: 'rgba(255, 255, 255, 0.2)' }, + showArea: true, + allowIntersection: false, + showLength: false, repeatMode: false, + useTreshold: newProps.useTreshold, + uom: newProps.uom, + geomType: newProps.measurement.geomType, + ...uomOptions, icon: new L.DivIcon({ iconSize: new L.Point(8, 8), className: 'leaflet-div-icon leaflet-editing-icon' @@ -239,6 +398,7 @@ class MeasurementSupport extends React.Component { this.removeLastLayer(); this.props.map.off('draw:created', this.onDrawCreated, this); this.props.map.off('draw:drawstart', this.onDrawStart, this); + this.props.map.off('mousemove', this.updateBearing, this); this.props.map.off('click', this.mapClickHandler, this); if (this.props.updateOnMouseMove) { this.props.map.off('mousemove', this.updateMeasurementResults, this); @@ -255,6 +415,57 @@ class MeasurementSupport extends React.Component { this.props.map.removeLayer(this.arcLayer); } } + + uomLengthOptions = (props) => { + let { + unit + } = props.uom.length; + const metric = unit === "m" || unit === "km"; // false = miles&yards + const nautic = unit === "nm"; + const feet = unit === "ft"; + return { + metric, + nautic, + feet + }; + } + uomAreaOptions = (props) => { + let { + unit + } = props.uom.area; + const metric = unit === "sqm" || unit === "sqkm"; // false = miles + const nautic = unit === "sqnm"; + const feet = unit === "sqft"; + return { + metric, + nautic, + feet + }; + } + + calculateBearing = () => { + let currentLatLng = this.drawControl._currentLatLng; + let bearing = 0; + let bearingMarkers = this.drawControl._markers; + let coords1 = [bearingMarkers[0].getLatLng().lng, bearingMarkers[0].getLatLng().lat]; + let coords2; + if (bearingMarkers.length === 1) { + coords2 = [currentLatLng.lng, currentLatLng.lat]; + } else if (bearingMarkers.length === 2) { + coords2 = [bearingMarkers[1].getLatLng().lng, bearingMarkers[1].getLatLng().lat]; + } + // in order to align the results between leaflet and openlayers the coords are repojected only for leaflet + coords1 = reproject(coords1, 'EPSG:4326', this.props.projection); + coords2 = reproject(coords2, 'EPSG:4326', this.props.projection); + // calculate the azimuth as base for bearing information + bearing = calculateAzimuth(coords1, coords2, this.props.projection); + return bearing; + } + updateBearing = () => { + if (this.props.measurement.geomType === 'Bearing' && this.drawControl._markers && this.drawControl._markers.length > 0) { + this.drawControl.setOptions({ bearing: this.calculateBearing() }); + } + } } module.exports = MeasurementSupport; diff --git a/web/client/components/map/leaflet/__tests__/MeasurementSupport-test.jsx b/web/client/components/map/leaflet/__tests__/MeasurementSupport-test.jsx index 60c22fc9d0..a7b8c9d7b3 100644 --- a/web/client/components/map/leaflet/__tests__/MeasurementSupport-test.jsx +++ b/web/client/components/map/leaflet/__tests__/MeasurementSupport-test.jsx @@ -11,7 +11,24 @@ var React = require('react'); var ReactDOM = require('react-dom'); var L = require('leaflet'); var MeasurementSupport = require('../MeasurementSupport'); - +let defaultPrecision = { + km: 2, + ha: 2, + m: 2, + mi: 2, + ac: 2, + yd: 0, + ft: 0, + nm: 2, + sqkm: 2, + sqha: 2, + sqm: 2, + sqmi: 2, + sqac: 2, + sqyd: 2, + sqft: 2, + sqnm: 2 +}; describe('Leaflet MeasurementSupport', () => { var msNode; function getMapLayersNum(map) { @@ -286,4 +303,214 @@ describe('Leaflet MeasurementSupport', () => { document.getElementById('map').click(); }); + it('test L.GeometryUtil.readableDistance with Bearing', () => { + const distance = 1; + const isMetric = true; + const isFeet = false; + const isNauticalMile = false; + const precision = null; + const options = { + geomType: "Bearing", + bearing: "5° N 4° E", + useTreshold: true, + uom: { + length: { + unit: "m", + label: "m" + } + } + }; + let distanceStr = L.GeometryUtil.readableDistance(distance, isMetric, isFeet, isNauticalMile, precision, options); + expect(distanceStr).toBe("5° N 4° E"); + }); + it('test L.GeometryUtil.readableDistance length with trehsold', () => { + const distance = 1; + const isMetric = true; + const isFeet = false; + const isNauticalMile = false; + const precision = null; + const options = { + geomType: "LineString", + bearing: 0, + useTreshold: true, + uom: { + length: { + unit: "km", + label: "km" + } + } + }; + let distanceStr = L.GeometryUtil.readableDistance(distance, isMetric, isFeet, isNauticalMile, precision, options); + expect(distanceStr).toBe("1.00 m"); + + distanceStr = L.GeometryUtil.readableDistance(distance * 1E4, isMetric, isFeet, isNauticalMile, precision, options); + expect(distanceStr).toBe("10.00 km"); + }); + it('test L.GeometryUtil.readableDistance metric length with no trehsold', () => { + const distance = 1000; + const isMetric = true; + const isFeet = false; + const isNauticalMile = false; + const precision = null; + const options = { + geomType: "LineString", + bearing: 0, + useTreshold: false, + uom: { + length: { + unit: "km", + label: "km" + } + } + }; + let distanceStr = L.GeometryUtil.readableDistance(distance, isMetric, isFeet, isNauticalMile, precision, options); + expect(distanceStr).toBe("1.00 km"); + }); + it('test L.GeometryUtil.readableDistance imperial length with trehsold', () => { + const distance = 10; + const isMetric = false; + const isFeet = false; + const isNauticalMile = false; + const precision = null; + const options = { + geomType: "LineString", + bearing: 0, + useTreshold: true, + uom: { + length: { + unit: "mi", + label: "mi" + } + } + }; + let distanceStr = L.GeometryUtil.readableDistance(distance, isMetric, isFeet, isNauticalMile, precision, options); + expect(distanceStr).toBe("11 yd"); + distanceStr = L.GeometryUtil.readableDistance(distance * 1e4, isMetric, isFeet, isNauticalMile, precision, options); + expect(distanceStr).toBe("62.14 mi"); + }); + it('test L.GeometryUtil.readableArea imperial length with trehsold', () => { + const area = 100000; + const isMetric = false; + const precision = null; + const options = { + geomType: "LineString", + bearing: 0, + useTreshold: true, + uom: { + area: { + unit: "sqmi", + label: "mi²" + } + } + }; + let areaStr = L.GeometryUtil.readableArea(area, isMetric, precision, options); + expect(areaStr).toBe("119600.00 yd²"); + areaStr = L.GeometryUtil.readableArea(area * 1e6, isMetric, precision, options); + expect(areaStr).toBe("38610.22 mi²"); + }); + it('test L.GeometryUtil.readableArea metric length with trehsold', () => { + const area = 100000; + const isMetric = true; + const precision = null; + const options = { + geomType: "LineString", + bearing: 0, + useTreshold: true, + uom: { + area: { + unit: "sqkm", + label: "km²" + } + } + }; + let areaStr = L.GeometryUtil.readableArea(area, isMetric, precision, options); + expect(areaStr).toBe("100000.00 m²"); + areaStr = L.GeometryUtil.readableArea(area * 1e6, isMetric, precision, options); + expect(areaStr).toBe("100000.00 km²"); + }); + it('test L.GeometryUtil.readableArea metric length with no trehsold', () => { + const area = 100000; + const isMetric = true; + const precision = null; + const options = { + geomType: "LineString", + bearing: 0, + useTreshold: false, + uom: { + area: { + unit: "sqkm", + label: "km²" + } + } + }; + let areaStr = L.GeometryUtil.readableArea(area, isMetric, precision, options); + expect(areaStr).toBe("0.10 km²"); + areaStr = L.GeometryUtil.readableArea(area * 1e3, isMetric, precision, options); + expect(areaStr).toBe("100.00 km²"); + }); + it('test L.getMeasureWithTreshold', () => { + const value = 100000; + const threshold = 1000; + const precision = defaultPrecision; + const source = "m"; + const dest = "km"; + const sourceLabel = "m"; + const destLabel = "km"; + + // value > treshold + let areaStr = L.getMeasureWithTreshold(value, threshold, source, dest, precision, sourceLabel, destLabel); + expect(areaStr).toBe("100.00 km"); + // value < treshold + areaStr = L.getMeasureWithTreshold(value, threshold * 1e3, source, dest, precision, sourceLabel, destLabel); + expect(areaStr).toBe("100000.00 m"); + }); + it('test L.Draw.Polygon.prototype._getMeasurementString', () => { + L.Draw.Polygon.prototype.options = { + metric: true, + precision: undefined, + useTreshold: true, + uom: { + area: { + unit: "sqkm", + label: "km²" + } + }, + showLength: false + }; + L.Draw.Polygon.prototype._area = 1000; + + let areaStr = L.Draw.Polygon.prototype._getMeasurementString(); + expect(areaStr).toBe("1000.00 m²"); + + }); + it('test L.Draw.Polyline.prototype._getMeasurementString', () => { + L.Draw.Polyline.prototype.options = { + metric: true, + feet: false, + nautic: false, + precision: undefined, + useTreshold: true, + uom: { + length: { + unit: "km", + label: "km" + } + }, + showLength: false + }; + let map = L.map("map", { + center: [51.505, -0.09], + zoom: 13 + }); + L.Draw.Polyline.prototype._map = map; + L.Draw.Polyline.prototype._measurementRunningTotal = 0; + L.Draw.Polyline.prototype._currentLatLng = L.latLng([50.5, 30.5]); + L.Draw.Polyline.prototype._markers = [L.marker([50.5, 30.5]), L.marker([52.5, 30.5])]; + + let distanceStr = L.Draw.Polyline.prototype._getMeasurementString(); + expect(distanceStr).toBe("222.39 km"); + + }); + + }); diff --git a/web/client/components/map/openlayers/MeasurementSupport.jsx b/web/client/components/map/openlayers/MeasurementSupport.jsx index 5a84729767..f23b6a6b88 100644 --- a/web/client/components/map/openlayers/MeasurementSupport.jsx +++ b/web/client/components/map/openlayers/MeasurementSupport.jsx @@ -13,7 +13,7 @@ const assign = require('object-assign'); const ol = require('openlayers'); const wgs84Sphere = new ol.Sphere(6378137); const {reprojectGeoJson, reproject, calculateAzimuth, calculateDistance, transformLineToArcs} = require('../../../utils/CoordinatesUtils'); -const {getFormattedLength, getFormattedArea, getFormattedBearingValue} = require('../../../utils/MeasureUtils'); +const {convertUom, getFormattedBearingValue} = require('../../../utils/MeasureUtils'); const {getMessageById} = require('../../../utils/LocaleUtils'); class MeasurementSupport extends React.Component { @@ -345,7 +345,7 @@ class MeasurementSupport extends React.Component { const reprojectedCoords = this.reprojectedCoordinates(sketchCoords); const length = calculateDistance(reprojectedCoords, this.props.measurement.lengthFormula); const {label, unit} = this.props.uom && this.props.uom.length; - const output = round(getFormattedLength(unit, length), 2); + const output = round(convertUom(length, "m", unit), 2); return output + " " + (label); }; @@ -357,7 +357,7 @@ class MeasurementSupport extends React.Component { formatArea = (polygon) => { const area = this.calculateGeodesicArea(polygon.getLinearRing(0).getCoordinates()); const {label, unit} = this.props.uom && this.props.uom.area; - const output = round(getFormattedArea(unit, area), 2); + const output = round(convertUom(area, "sqm", unit), 2); return output + " " + label; }; diff --git a/web/client/components/mapcontrols/measure/MeasureComponent.jsx b/web/client/components/mapcontrols/measure/MeasureComponent.jsx index eed3c39d40..4456ddc440 100644 --- a/web/client/components/mapcontrols/measure/MeasureComponent.jsx +++ b/web/client/components/mapcontrols/measure/MeasureComponent.jsx @@ -13,7 +13,7 @@ const ToggleButton = require('../../buttons/ToggleButton'); const NumberFormat = require('../../I18N/Number'); const Message = require('../../I18N/Message'); const {DropdownList} = require('react-widgets'); -const measureUtils = require('../../../utils/MeasureUtils'); +const {convertUom, getFormattedBearingValue} = require('../../../utils/MeasureUtils'); const localeUtils = require('../../../utils/LocaleUtils'); const {isEqual, round} = require('lodash'); @@ -104,9 +104,9 @@ class MeasureComponent extends React.Component { lengthLabel: <Message msgId="measureComponent.lengthLabel"/>, areaLabel: <Message msgId="measureComponent.areaLabel"/>, bearingLabel: <Message msgId="measureComponent.bearingLabel"/>, - formatLength: (uom, value) => measureUtils.getFormattedLength(uom, value), - formatArea: (uom, value) => measureUtils.getFormattedArea(uom, value), - formatBearing: (value) => measureUtils.getFormattedBearingValue(round(value || 0, 6)), + formatLength: (uom, value) => convertUom(value, "m", uom), + formatArea: (uom, value) => convertUom(value, "sqm", uom), + formatBearing: (value) => getFormattedBearingValue(round(value || 0, 6)), onChangeUom: () => {} }; diff --git a/web/client/plugins/map/index.js b/web/client/plugins/map/index.js index 901ebf2fec..3b71837e61 100644 --- a/web/client/plugins/map/index.js +++ b/web/client/plugins/map/index.js @@ -47,6 +47,7 @@ module.exports = (mapType, actions) => { const MeasurementSupport = connect((state) => ({ measurement: state.measurement || {}, + useTreshold: state.measurement && state.measurement.useTreshold || null, uom: state.measurement && state.measurement.uom || { length: {unit: 'm', label: 'm'}, area: {unit: 'sqm', label: 'm²'} diff --git a/web/client/utils/MeasureUtils.js b/web/client/utils/MeasureUtils.js index 8012b4cdae..390454b5c4 100644 --- a/web/client/utils/MeasureUtils.js +++ b/web/client/utils/MeasureUtils.js @@ -33,83 +33,116 @@ function getFormattedBearingValue(azimuth = 0) { return bearing; } -function mToft(length) { - return length * 3.28084; -} - -function mTokm(length) { - return length * 0.001; -} - -function mTomi(length) { - return length * 0.000621371; -} - -function mTonm(length) { - return length * 0.000539956803; -} - -function sqmTosqft(area) { - return area * 10.7639; -} -function sqmTosqkm(area) { - return area * 0.000001; -} - -function sqmTosqmi(area) { - return area * 0.000000386102159; -} -function sqmTosqnm(area) { - return area * 0.00000029155; -} - -function getFormattedLength(unit = "m", length = 0) { - switch (unit) { - case 'm': - return length; - case 'ft': - return mToft(length); - case 'km': - return mTokm(length); - case 'mi': - return mTomi(length); - case 'nm': - return mTonm(length); - default: - return length; +const CONVERSION_RATE = { + // length + "yd": { + "ft": 3, + "m": 0.9144, + "km": 0.0009144, + "yd": 1, + "mi": 0.00056818181818, + "nm": 0.00049373650107 + }, + "ft": { + "ft": 1, + "m": 0.3048, + "km": 0.0003048, + "yd": 0.33333333333334, + "mi": 0.0001893932, + "nm": 0.000164579 + }, + "m": { + "ft": 3.28084, + "m": 1, + "km": 0.001, + "yd": 1.0936132983377, + "mi": 0.000621371, + "nm": 0.000539956803 + }, + "km": { + "ft": 3280.84, + "m": 1000, + "km": 1, + "yd": 1093.6132983377, + "mi": 0.62137121212121, + "nm": 0.53995682073433948212 + }, + "mi": { + "ft": 5280.0001689599821475, + "m": 1609.3440514990027168, + "km": 1.6093440514990027257, + "yd": 1760, + "mi": 1, + "nm": 0.86897626970788488521 + }, + "nm": { + "ft": 6076.1156799999789655, + "m": 1852.0000592639937622, + "km": 1.8520000592639938031, + "yd": 2025.3718285214, + "mi": 1.1507794848484809158, + "nm": 1 + }, + "sqft": { + "sqft": 1, + "sqm": 0.09290304, + "sqkm": 9.2903043596611E-8, + "sqmi": 3.587E-8, + "sqnm": 2.7051601137505E-8 + }, + "sqyd": { + "sqft": 8.9999247491639, + "sqm": 0.83612040133779, + "sqkm": 8.3612040133779e-7, + "sqyd": 1, + "sqmi": 3.228278917579e-7, + "sqnm": 2.4346237458194e-7 + }, + // area + "sqm": { + "sqft": 10.76391, + "sqm": 1, + "sqkm": 1.0E-6, + "sqyd": 1.196, + "sqmi": 3.8610215854245e-7, + "sqnm": 2.91181e-7 + }, + "sqkm": { + "sqft": 10763910, + "sqm": 1.0E6, + "sqkm": 1, + "sqyd": 1196000, + "sqmi": 0.38610215854245, + "sqnm": 0.291181 + }, + "sqmi": { + "sqft": 27878398.920726, + "sqm": 2589988.110336, + "sqkm": 2.589988110336, + "sqyd": 27878398.920726, + "sqmi": 1, + "sqnm": 0.75415532795574 + }, + "sqnm": { + "sqft": 36966388.603652, + "sqm": 3434290.0120544, + "sqkm": 3.4342900120544, + "sqyd": 36966388.603652, + "sqmi": 1.325986786715, + "sqnm": 1 } -} +}; -function getFormattedArea(unit = "sqm", area = 0) { - switch (unit) { - case 'sqm': - return area; - case 'sqft': - return sqmTosqft(area); - case 'sqkm': - return sqmTosqkm(area); - case 'sqmi': - return sqmTosqmi(area); - case 'sqnm': - return sqmTosqnm(area); - default: - return area; +function convertUom(value, source = "m", dest = "m") { + if (!!CONVERSION_RATE[source] && !!CONVERSION_RATE[source][dest]) { + return value * CONVERSION_RATE[source][dest]; } + return value; } - module.exports = { + convertUom, getFormattedBearingValue, - getFormattedLength, - getFormattedArea, - degToDms, - mToft, - mTokm, - mTomi, - mTonm, - sqmTosqmi, - sqmTosqkm, - sqmTosqnm, - sqmTosqft + degToDms }; diff --git a/web/client/utils/__tests__/MeasureUtils-test.js b/web/client/utils/__tests__/MeasureUtils-test.js index 614a9755b0..272f9943d2 100644 --- a/web/client/utils/__tests__/MeasureUtils-test.js +++ b/web/client/utils/__tests__/MeasureUtils-test.js @@ -8,17 +8,8 @@ const expect = require('expect'); const { getFormattedBearingValue, - getFormattedLength, - getFormattedArea, degToDms, - mToft, - mTokm, - mTomi, - mTonm, - sqmTosqmi, - sqmTosqkm, - sqmTosqnm, - sqmTosqft + convertUom } = require('../MeasureUtils'); @@ -28,67 +19,60 @@ describe('MeasureUtils', () => { }); afterEach(() => { + }); + it('test conversion km to mi', () => { + const val = convertUom(1, "km", "mi"); + expect(val).toBe(0.62137121212121); }); it('test conversion meters to feet', () => { - const val = mToft(1); + const val = convertUom(1, "m", "ft"); expect(val).toBe(3.28084); }); it('test conversion meters to kilometers', () => { - const val = mTokm(1); + const val = convertUom(1, "m", "km"); expect(val).toBe(0.001); }); it('test conversion meters to miles', () => { - const val = mTomi(1); + const val = convertUom(1, "m", "mi"); expect(val).toBe(0.000621371); }); it('test conversion meters to nauticalmiles', () => { - const val = mTonm(1); + const val = convertUom(1, "m", "nm"); expect(val).toBe(0.000539956803); }); it('test conversion squaremeters to squarefeet', () => { - const val = sqmTosqft(1); - expect(val).toBe(10.7639); + const val = convertUom(1, "sqm", "sqft"); + expect(val).toBe(10.76391); }); it('test conversion squaremeters to squarekilometers', () => { - const val = sqmTosqkm(1); + const val = convertUom(1, "sqm", "sqkm"); expect(val).toBe(0.000001); }); it('test conversion squaremeters to squaremiles', () => { - const val = sqmTosqmi(1); - expect(val).toBe(0.000000386102159); + const val = convertUom(1, "sqm", "sqmi"); + expect(val).toBe(3.8610215854245e-7); }); it('test conversion squaremeters to squarenauticalmiles', () => { - const val = sqmTosqnm(1); - expect(val).toBe(0.00000029155); - }); - it('test getFormattedLength', () => { - let val = getFormattedLength("m", 1); - expect(val).toBe(1); - val = getFormattedLength(undefined, 1); - expect(val).toBe(1); - val = getFormattedLength("ft", 1); - expect(val).toBe(3.28084); - val = getFormattedLength("km", 1); - expect(val).toBe(0.001); - val = getFormattedLength("mi", 1); - expect(val).toBe(0.000621371); - val = getFormattedLength("nm", 1); - expect(val).toBe(0.000539956803); + const val = convertUom(1, "sqm", "sqnm"); + expect(val).toBe(2.91181e-7); }); - it('test getFormattedArea', () => { - let val = getFormattedArea("sqm", 1); - expect(val).toBe(1); - val = getFormattedArea(undefined, 1); - expect(val).toBe(1); - val = getFormattedArea("sqft", 1); - expect(val).toBe(10.7639); - val = getFormattedArea("sqkm", 1); - expect(val).toBe(0.000001); - val = getFormattedArea("sqmi", 1); - expect(val).toBe(0.000000386102159); - val = getFormattedArea("sqnm", 1); - expect(val).toBe(0.00000029155); + it('test conversion squarefeets to squarekilometers', () => { + const val = convertUom(1, "sqft", "sqkm"); + expect(val).toBe(9.2903043596611e-8); + }); + it('test conversion squarekilometers to squaremiles', () => { + const val = convertUom(1, "sqkm", "sqmi"); + expect(val).toBe(0.38610215854245); + }); + it('test conversion squaremiles to squarenauticalmiles', () => { + const val = convertUom(1, "sqmi", "sqnm"); + expect(val).toBe(0.75415532795574); }); + it('test conversion squarenauticalmiles to squaremiles', () => { + const val = convertUom(1, "sqnm", "sqmi"); + expect(val).toBe(1.325986786715); + }); + it('test degToDms', () => { let val = degToDms(1.111); expect(val).toBe("1° 6' 39'' ");