diff --git a/data/core.yaml b/data/core.yaml index 08021a33f..0e102c74f 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -309,7 +309,8 @@ en: title: Measurement selected: "{n} selected" geometry: Geometry - closed: closed + closed_line: closed line + closed_area: closed area center: Center perimeter: Perimeter length: Length @@ -1055,3 +1056,24 @@ en: history: "Toggle history panel" location: "Toggle location panel" measurement: "Toggle measurement panel" + units: + feet: "{quantity} ft" + miles: "{quantity} mi" + square_feet: "{quantity} sq ft" + square_miles: "{quantity} sq mi" + acres: "{quantity} ac" + meters: "{quantity} m" + kilometers: "{quantity} km" + square_meters: "{quantity} m²" + square_kilometers: "{quantity} km²" + hectares: "{quantity} ha" + area_pair: "{area1} ({area2})" + arcdegrees: "{quantity}°" + arcminutes: "{quantity}′" + arcseconds: "{quantity}″" + north: "N" + south: "S" + east: "E" + west: "W" + coordinate: "{coordinate}{direction}" + coordinate_pair: "{latitude}, {longitude}" diff --git a/dist/locales/en.json b/dist/locales/en.json index 6fdd5b16b..c008d91c2 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -389,7 +389,8 @@ "title": "Measurement", "selected": "{n} selected", "geometry": "Geometry", - "closed": "closed", + "closed_line": "closed line", + "closed_area": "closed area", "center": "Center", "perimeter": "Perimeter", "length": "Length", @@ -1221,6 +1222,28 @@ } } }, + "units": { + "feet": "{quantity} ft", + "miles": "{quantity} mi", + "square_feet": "{quantity} sq ft", + "square_miles": "{quantity} sq mi", + "acres": "{quantity} ac", + "meters": "{quantity} m", + "kilometers": "{quantity} km", + "square_meters": "{quantity} m²", + "square_kilometers": "{quantity} km²", + "hectares": "{quantity} ha", + "area_pair": "{area1} ({area2})", + "arcdegrees": "{quantity}°", + "arcminutes": "{quantity}′", + "arcseconds": "{quantity}″", + "north": "N", + "south": "S", + "east": "E", + "west": "W", + "coordinate": "{coordinate}{direction}", + "coordinate_pair": "{latitude}, {longitude}" + }, "presets": { "categories": { "category-barrier": { diff --git a/modules/ui/feature_list.js b/modules/ui/feature_list.js index 4fc298731..ebd6448b9 100644 --- a/modules/ui/feature_list.js +++ b/modules/ui/feature_list.js @@ -7,6 +7,7 @@ import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import * as sexagesimal from '@mapbox/sexagesimal'; import { t } from '../util/locale'; +import { dmsCoordinatePair } from '../util/units'; import { geoExtent, geoChooseEdge } from '../geo'; import { modeSelect } from '../modes'; import { osmEntity } from '../osm'; @@ -143,7 +144,7 @@ export function uiFeatureList(context) { id: -1, geometry: 'point', type: t('inspector.location'), - name: loc[0].toFixed(6) + ', ' + loc[1].toFixed(6), + name: dmsCoordinatePair([loc[1], loc[0]]), location: loc }); } diff --git a/modules/ui/panels/location.js b/modules/ui/panels/location.js index 42bda702e..e79903c11 100644 --- a/modules/ui/panels/location.js +++ b/modules/ui/panels/location.js @@ -1,23 +1,12 @@ import _debounce from 'lodash-es/debounce'; +import { decimalCoordinatePair, dmsCoordinatePair } from '../../util/units'; import { t } from '../../util/locale'; import { services } from '../../services'; export function uiPanelLocation(context) { var currLocation = ''; - var OSM_PRECISION = 7; - - - function wrap(x, min, max) { - var d = max - min; - return ((x - min) % d + d) % d + min; - } - - - function clamp(x, min, max) { - return Math.max(min, Math.min(x, max)); - } function redraw(selection) { @@ -32,13 +21,11 @@ export function uiPanelLocation(context) { coord = context.map().center(); } - var coordStr = - clamp(coord[1], -90, 90).toFixed(OSM_PRECISION) + ', ' + - wrap(coord[0], -180, 180).toFixed(OSM_PRECISION); - list .append('li') - .text(coordStr); + .text(dmsCoordinatePair(coord)) + .append('li') + .text(decimalCoordinatePair(coord)); // Location Info selection diff --git a/modules/ui/panels/measurement.js b/modules/ui/panels/measurement.js index 48a6546bd..ff7369925 100644 --- a/modules/ui/panels/measurement.js +++ b/modules/ui/panels/measurement.js @@ -8,14 +8,15 @@ import { } from 'd3-geo'; import { t } from '../../util/locale'; +import { displayArea, displayLength, decimalCoordinatePair, dmsCoordinatePair } from '../../util/units'; import { geoExtent } from '../../geo'; import { utilDetect } from '../../util/detect'; export function uiPanelMeasurement(context) { - var isImperial = (utilDetect().locale.toLowerCase() === 'en-us'); - var OSM_PRECISION = 7; + var locale = utilDetect().locale, + isImperial = (locale.toLowerCase() === 'en-us'); function radiansToMeters(r) { @@ -51,75 +52,6 @@ export function uiPanelMeasurement(context) { } - function displayLength(m) { - var d = m * (isImperial ? 3.28084 : 1), - p, unit; - - if (isImperial) { - if (d >= 5280) { - d /= 5280; - unit = 'mi'; - } else { - unit = 'ft'; - } - } else { - if (d >= 1000) { - d /= 1000; - unit = 'km'; - } else { - unit = 'm'; - } - } - - // drop unnecessary precision - p = d > 1000 ? 0 : d > 100 ? 1 : 2; - - return String(d.toFixed(p)) + ' ' + unit; - } - - - function displayArea(m2) { - var d = m2 * (isImperial ? 10.7639111056 : 1), - d1, d2, p1, p2, unit1, unit2; - - if (isImperial) { - if (d >= 6969600) { // > 0.25mi² show mi² - d1 = d / 27878400; - unit1 = 'mi²'; - } else { - d1 = d; - unit1 = 'ft²'; - } - - if (d > 4356 && d < 43560000) { // 0.1 - 1000 acres - d2 = d / 43560; - unit2 = 'ac'; - } - - } else { - if (d >= 250000) { // > 0.25km² show km² - d1 = d / 1000000; - unit1 = 'km²'; - } else { - d1 = d; - unit1 = 'm²'; - } - - if (d > 1000 && d < 10000000) { // 0.1 - 1000 hectares - d2 = d / 10000; - unit2 = 'ha'; - } - } - - // drop unnecessary precision - p1 = d1 > 1000 ? 0 : d1 > 100 ? 1 : 2; - p2 = d2 > 1000 ? 0 : d2 > 100 ? 1 : 2; - - return String(d1.toFixed(p1)) + ' ' + unit1 + - (d2 ? ' (' + String(d2.toFixed(p2)) + ' ' + unit2 + ')' : ''); - } - - function redraw(selection) { var resolver = context.graph(); var selected = _filter(context.selectedIDs(), function(e) { return context.hasEntity(e); }); @@ -132,7 +64,7 @@ export function uiPanelMeasurement(context) { selection .append('h4') .attr('class', 'measurement-heading') - .text(singular || t('info_panels.measurement.selected', { n: selected.length })); + .text(singular || t('info_panels.measurement.selected', { n: selected.length.toLocaleString(locale) })); if (!selected.length) return; @@ -146,16 +78,17 @@ export function uiPanelMeasurement(context) { var list = selection .append('ul'); + var coordItem; // multiple features, just display extent center.. if (!singular) { - list + coordItem = list .append('li') - .text(t('info_panels.measurement.center') + ':') - .append('span') - .text( - center[1].toFixed(OSM_PRECISION) + ', ' + center[0].toFixed(OSM_PRECISION) - ); + .text(t('info_panels.measurement.center') + ':'); + coordItem.append('span') + .text(dmsCoordinatePair(center)); + coordItem.append('span') + .text(decimalCoordinatePair(center)); return; } @@ -175,7 +108,7 @@ export function uiPanelMeasurement(context) { .text(t('info_panels.measurement.geometry') + ':') .append('span') .text( - (closed ? t('info_panels.measurement.closed') + ' ' : '') + t('geometry.' + geometry) + closed ? t('info_panels.measurement.closed_' + geometry) : t('geometry.' + geometry) ); if (entity.type !== 'relation') { @@ -183,8 +116,7 @@ export function uiPanelMeasurement(context) { .append('li') .text(t('info_panels.measurement.node_count') + ':') .append('span') - .text(nodeCount(feature) - ); + .text(nodeCount(feature).toLocaleString(locale)); } if (closed) { @@ -193,7 +125,7 @@ export function uiPanelMeasurement(context) { .append('li') .text(t('info_panels.measurement.area') + ':') .append('span') - .text(displayArea(area)); + .text(displayArea(area, isImperial)); } @@ -201,15 +133,15 @@ export function uiPanelMeasurement(context) { .append('li') .text(lengthLabel + ':') .append('span') - .text(displayLength(length)); + .text(displayLength(length, isImperial)); - list + coordItem = list .append('li') - .text(t('info_panels.measurement.centroid') + ':') - .append('span') - .text( - centroid[1].toFixed(OSM_PRECISION) + ', ' + centroid[0].toFixed(OSM_PRECISION) - ); + .text(t('info_panels.measurement.centroid') + ':'); + coordItem.append('span') + .text(dmsCoordinatePair(centroid)); + coordItem.append('span') + .text(decimalCoordinatePair(centroid)); var toggle = isImperial ? 'imperial' : 'metric'; @@ -233,13 +165,13 @@ export function uiPanelMeasurement(context) { .append('span') .text(t('geometry.' + geometry)); - list + coordItem = list .append('li') - .text(centerLabel + ':') - .append('span') - .text( - center[1].toFixed(OSM_PRECISION) + ', ' + center[0].toFixed(OSM_PRECISION) - ); + .text(centerLabel + ':'); + coordItem.append('span') + .text(dmsCoordinatePair(center)); + coordItem.append('span') + .text(decimalCoordinatePair(center)); } } diff --git a/modules/ui/scale.js b/modules/ui/scale.js index fb7f869c1..953dc1f66 100644 --- a/modules/ui/scale.js +++ b/modules/ui/scale.js @@ -1,3 +1,4 @@ +import { displayLength } from '../util/units'; import { geoLonToMeters, geoMetersToLon } from '../geo'; import { utilDetect } from '../util/detect'; @@ -36,21 +37,7 @@ export function uiScale(context) { dLon = geoMetersToLon(scale.dist / conversion, lat); scale.px = Math.round(projection([loc1[0] + dLon, loc1[1]])[0]); - if (isImperial) { - if (scale.dist >= 5280) { - scale.dist /= 5280; - scale.text = String(scale.dist) + ' mi'; - } else { - scale.text = String(scale.dist) + ' ft'; - } - } else { - if (scale.dist >= 1000) { - scale.dist /= 1000; - scale.text = String(scale.dist) + ' km'; - } else { - scale.text = String(scale.dist) + ' m'; - } - } + scale.text = displayLength(scale.dist / conversion, isImperial); return scale; } diff --git a/modules/util/units.js b/modules/util/units.js new file mode 100644 index 000000000..b27e7c4b7 --- /dev/null +++ b/modules/util/units.js @@ -0,0 +1,155 @@ +import { t } from 'locale'; +import { utilDetect } from 'detect'; + +var OSM_PRECISION = 7; +var locale = utilDetect().locale; + +/** + * Returns a localized representation of the given length measurement. + * + * @param {Number} m area in meters + * @param {Boolean} isImperial true for U.S. customary units; false for metric + */ +export function displayLength(m, isImperial) { + var d = m * (isImperial ? 3.28084 : 1), + unit; + + if (isImperial) { + if (d >= 5280) { + d /= 5280; + unit = 'miles'; + } else { + unit = 'feet'; + } + } else { + if (d >= 1000) { + d /= 1000; + unit = 'kilometers'; + } else { + unit = 'meters'; + } + } + + return t('units.' + unit, { + quantity: d.toLocaleString(locale, { maximumSignificantDigits: 4 }) + }); +} + +/** + * Returns a localized representation of the given area measurement. + * + * @param {Number} m2 area in square meters + * @param {Boolean} isImperial true for U.S. customary units; false for metric + */ +export function displayArea(m2, isImperial) { + var d = m2 * (isImperial ? 10.7639111056 : 1), + d1, d2, unit1, unit2, area; + + if (isImperial) { + if (d >= 6969600) { // > 0.25mi² show mi² + d1 = d / 27878400; + unit1 = 'square_miles'; + } else { + d1 = d; + unit1 = 'square_feet'; + } + + if (d > 4356 && d < 43560000) { // 0.1 - 1000 acres + d2 = d / 43560; + unit2 = 'acres'; + } + + } else { + if (d >= 250000) { // > 0.25km² show km² + d1 = d / 1000000; + unit1 = 'square_kilometers'; + } else { + d1 = d; + unit1 = 'square_meters'; + } + + if (d > 1000 && d < 10000000) { // 0.1 - 1000 hectares + d2 = d / 10000; + unit2 = 'hectares'; + } + } + + area = t('units.' + unit1, { + quantity: d1.toLocaleString(locale, { maximumSignificantDigits: 4 }) + }); + + if (d2) { + return t('units.area_pair', { + area1: area, + area2: t('units.' + unit2, { + quantity: d2.toLocaleString(locale, { maximumSignificantDigits: 2 }) + }) + }); + } else { + return area; + } +} + +function wrap(x, min, max) { + var d = max - min; + return ((x - min) % d + d) % d + min; +} + +function clamp(x, min, max) { + return Math.max(min, Math.min(x, max)); +} + +function displayCoordinate(deg, pos, neg) { + var min = (Math.abs(deg) - Math.floor(Math.abs(deg))) * 60, + sec = (min - Math.floor(min)) * 60, + displayDegrees = t('units.arcdegrees', { + quantity: Math.floor(Math.abs(deg)).toLocaleString(locale) + }), + displayCoordinate; + + if (Math.floor(sec) > 0) { + displayCoordinate = displayDegrees + + t('units.arcminutes', { quantity: Math.floor(min).toLocaleString(locale) }) + + t('units.arcseconds', { quantity: Math.round(sec).toLocaleString(locale) }); + } else if (Math.floor(min) > 0) { + displayCoordinate = displayDegrees + + t('units.arcminutes', { quantity: Math.round(min).toLocaleString(locale) }); + } else { + displayCoordinate = t('units.arcdegrees', { + quantity: Math.round(Math.abs(deg)).toLocaleString(locale) + }); + } + + if (deg === 0) { + return displayCoordinate; + } else { + return t('units.coordinate', { + coordinate: displayCoordinate, + direction: t('units.' + (deg > 0 ? pos : neg)) + }); + } +} + +/** + * Returns given coordinate pair in degree-minute-second format. + * + * @param {Array} coord longitude and latitude + */ +export function dmsCoordinatePair(coord) { + return t('units.coordinate_pair', { + latitude: displayCoordinate(clamp(coord[1], -90, 90), 'north', 'south'), + longitude: displayCoordinate(wrap(coord[0], -180, 180), 'east', 'west') + }); +} + +/** + * Returns the given coordinate pair in decimal format. + * + * @param {Array} coord longitude and latitude + */ +export function decimalCoordinatePair(coord) { + return t('units.coordinate_pair', { + latitude: clamp(coord[1], -90, 90).toLocaleString(locale, { maximumFractionDigits: OSM_PRECISION }), + longitude: wrap(coord[0], -180, 180).toLocaleString(locale, { maximumFractionDigits: OSM_PRECISION }) + }); +}