diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d6adc9ab1..d90dd6ae77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - The mapInteractor cancelOnMove option can now take a movement threshold (#1058) - GCS can now be specified in pointSearch, boxSearch, and polygonSearch (#1051) - Added a track feature (#1040) +- Added a geo.gui.scaleWidget.formatUnit utility function (#1048) ## Version 0.19.8 diff --git a/examples/measure/main.js b/examples/measure/main.js index d6701006ee..7399bbd3cc 100644 --- a/examples/measure/main.js +++ b/examples/measure/main.js @@ -49,36 +49,6 @@ geo.osmLayer.tileSources['false'] = { url: '/data/white.jpg', attribution: '' }; -// Define a decimal miles unit -geo.gui.scaleWidget.unitsTable['decmiles'] = [ - {unit: 'mi', scale: 1609.344} -]; -// Define area units -var areaUnitsTable = { - si: [ - {unit: 'nm\xB2', scale: 1e-18}, - {unit: '\u03BCm\xB2', scale: 1e-12}, - {unit: 'mm\xB2', scale: 1e-6}, - {unit: 'm\xB2', scale: 1}, - {unit: 'km\xB2', scale: 1e6} - ], - hectares: [ - {unit: 'ha', scale: 1e4} - ], - decmiles: [ - {unit: 'mi\xB2', scale: 1609.344 * 1609.344} - ], - miles: [ - {unit: 'in\xB2', scale: 0.0254 * 0.0254}, - {unit: 'ft\xB2', scale: 0.3048 * 0.3048}, - {unit: 'mi\xB2', scale: 1609.344 * 1609.344} - ], - acres: [ - {unit: 'pl', scale: 0.3048 * 0.3048 * 16.5 * 16.5}, - {unit: 'rd', scale: 1609.344 * 1609.344 / 640 / 4}, - {unit: 'ac', scale: 1609.344 * 1609.344 / 640} - ] -}; var map, mapLayer, layer, fromButtonSelect, fromGeojsonUpdate; @@ -418,47 +388,6 @@ function annotationArea(annotation) { return Math.abs(area); } -/** - * Format a unit with at least three siginfiicant figures. - * - * @param {number} val The value. A valance or area in base units. - * @param {string} unit The name of the unit system. - * @param {object[]} [table] The table of the unit system. - * @returns {string} A formatted string or `undefined`. - */ -function formatUnit(val, unit, table) { - if (val === undefined || val === null) { - return; - } - table = table || geo.gui.scaleWidget.unitsTable; - if (!table || !table[unit]) { - return; - } - unit = table[unit]; - let pos; - for (pos = 0; pos < unit.length - 1; pos += 1) { - if (val < unit[pos + 1].scale) { - break; - } - } - unit = unit[pos]; - val /= unit.scale; - let digits = Math.max(0, -Math.ceil(Math.log10(val)) + 3); - if (digits > 10) { - return; - } - let result = val.toFixed(digits); - if (digits) { - while (result.substr(result.length - 1) === '0') { - result = result.substr(0, result.length - 1); - } - if (result.substr(result.length - 1) === '.') { - result = result.substr(0, result.length - 1); - } - } - return result + ' ' + unit.unit; -} - /** * Add the units to the name of the annotation if appropriate. * @@ -477,11 +406,11 @@ function modifyAnnotation(annotation) { area = annotationArea(annotation); var result = s_labelRecord(); if (result) { - dist = formatUnit(dist, query.distunit || 'decmiles'); + dist = geo.gui.scaleWidget.formatUnit(dist, query.distunit || 'decmiles'); if (dist) { result.text += ' - ' + dist; } - area = formatUnit(area, query.areaunit || 'decmiles', areaUnitsTable); + area = geo.gui.scaleWidget.formatUnit(area, query.areaunit || 'decmiles', geo.gui.scaleWidget.areaUnitsTable); if (area) { result.text += ' - ' + area; } @@ -527,20 +456,20 @@ function labelVertices(annotation, annotationIndex, labels) { pts.forEach((p, i) => { if (i) { labels.push({ - text: formatUnit(tally[i], query.distunit || 'decmiles') || '', + text: geo.gui.scaleWidget.formatUnit(tally[i], query.distunit || 'decmiles') || '', position: p, style: Object.assign({}, style, {offset: {x: 12, y: 0}, textAlign: 'left'}) }); } if (i !== pts.length - 1) { labels.push({ - text: formatUnit(tally[tally.length - 1] - tally[i], query.distunit || 'decmiles') || '', + text: geo.gui.scaleWidget.formatUnit(tally[tally.length - 1] - tally[i], query.distunit || 'decmiles') || '', position: p, style: Object.assign({}, style, {offset: {x: -12, y: 0}, textAlign: 'right'}) }); let p1 = {x: (pts[i + 1].x + p.x) / 2, y: (pts[i + 1].y + p.y) / 2}; labels.push({ - text: formatUnit(dist[i], query.distunit || 'decmiles') || '', + text: geo.gui.scaleWidget.formatUnit(dist[i], query.distunit || 'decmiles') || '', position: p1, style: Object.assign({}, style, {offset: {x: 10, y: 0}, textAlign: 'left'}) }); @@ -582,8 +511,8 @@ function handleAnnotationChange(evt) { area = annotationArea(annotation); if (dist) { totaldist += dist; } if (area) { totalarea += area; } - dist = formatUnit(dist, query.distunit || 'decmiles') || ''; - area = formatUnit(area, query.areaunit || 'decmiles', areaUnitsTable); + dist = geo.gui.scaleWidget.formatUnit(dist, query.distunit || 'decmiles') || ''; + area = geo.gui.scaleWidget.formatUnit(area, query.areaunit || 'decmiles', geo.gui.scaleWidget.areaUnitsTable); if (area) { dist = (dist ? dist + ' - ' : '') + area; } @@ -603,8 +532,8 @@ function handleAnnotationChange(evt) { entry.find('.entry-dist').text(dist); $('#annotationlist').append(entry); }); - let dist = formatUnit(totaldist || undefined, query.distunit || 'decmiles') || ''; - let area = formatUnit(totalarea || undefined, query.areaunit || 'decmiles', areaUnitsTable); + let dist = geo.gui.scaleWidget.formatUnit(totaldist || undefined, query.distunit || 'decmiles') || ''; + let area = geo.gui.scaleWidget.formatUnit(totalarea || undefined, query.areaunit || 'decmiles', geo.gui.scaleWidget.areaUnitsTable); if (area) { dist = (dist ? dist + ' - ' : '') + area; } diff --git a/src/ui/scaleWidget.js b/src/ui/scaleWidget.js index 01a0f791aa..686029c594 100644 --- a/src/ui/scaleWidget.js +++ b/src/ui/scaleWidget.js @@ -52,6 +52,24 @@ require('./scaleWidget.styl'); * `mutliple`. */ +/** + * For a unit table, the records are ordered smallest scale to largest scale. + * The smallest unit can be repeated to have different rounding behavior for + * values less than 1 and values greater than or equal to 1. + * + * @typedef {object} geo.gui.scaleWidget.unitTableRecord + * @property {string} unit The display name of the unit. + * @property {number} scale The size of the unit in base unit. + * @property {number} [basis=10] The number of units in the next greater unit + * if not a power of 10. + * @property {object[]} [multiples] A list of multiplier values to round to + * when rounding is used. The list should probably include a multiple of 1. + * Default is 1, 1.5, 2, 3, 5, 8. + * @property {number} multiples.multiple A factor to round to. + * @property {number} multiples.digit The number of digits to preserve when + * rounding. + */ + /** * Create a new instance of class geo.gui.scaleWidget. * @@ -314,12 +332,21 @@ var scaleWidget = function (arg) { inherit(scaleWidget, svgWidget); -/* The unitsTable has predefined unit sets. Each entry is an array that must - * be in ascending order. */ +/** + * The unitsTable has predefined unit sets for a base unit of one meter. Each + * entry is an array that must be in ascending order. Use unicode in strings, + * not html entities. It makes it more reusable. + * @name unitsTable + * @property unitsTable {object} The key names are the names of unit systems, + * such as `si`. + * @property unitsTable.unit {geo.gui.scaleWidget.unitTableRecord[]} A list of + * units within the unit system from smallest to largest. + * @memberof geo.gui.scaleWidget + */ scaleWidget.unitsTable = { si: [ {unit: 'nm', scale: 1e-9}, - {unit: 'μm', scale: 1e-6}, + {unit: '\u03BCm', scale: 1e-6}, {unit: 'mm', scale: 0.001}, {unit: 'm', scale: 1}, {unit: 'km', scale: 1000} @@ -346,8 +373,98 @@ scaleWidget.unitsTable = { }, {unit: 'ft', scale: 0.3048}, {unit: 'mi', scale: 1609.344} + ], + decmiles: [ // decimal miles + {unit: 'mi', scale: 1609.344} + ] +}; + +/** + * The areaUnitsTable has predefined unit sets for a base unit of one square + * meter. Each entry is an array that must be in ascending order. This table + * can be passed to formatUnit. + * @name areaUnitsTable + * @property areaUnitsTable {object} The key names are the names of unit + * systems, such as `si`. + * @property areaUnitsTable.unit {geo.gui.scaleWidget.unitTableRecord[]} A list + * of units within the unit system from smallest to largest. + * @memberof geo.gui.scaleWidget + */ +scaleWidget.areaUnitsTable = { + si: [ + {unit: 'nm\xB2', scale: 1e-18}, + {unit: '\u03BCm\xB2', scale: 1e-12}, + {unit: 'mm\xB2', scale: 1e-6}, + {unit: 'm\xB2', scale: 1}, + {unit: 'km\xB2', scale: 1e6} + ], + hectares: [ + {unit: 'ha', scale: 1e4} + ], + decmiles: [ // decimal square miles + {unit: 'mi\xB2', scale: 1609.344 * 1609.344} + ], + miles: [ + {unit: 'in\xB2', scale: 0.0254 * 0.0254}, + {unit: 'ft\xB2', scale: 0.3048 * 0.3048}, + {unit: 'mi\xB2', scale: 1609.344 * 1609.344} + ], + acres: [ + {unit: 'pl', scale: 0.3048 * 0.3048 * 16.5 * 16.5}, + {unit: 'rd', scale: 1609.344 * 1609.344 / 640 / 4}, + {unit: 'ac', scale: 1609.344 * 1609.344 / 640} ] }; +/** + * Format a unit with a specified number of significant figures. Given a value + * in base units, such as meters, this will return a string with appropriate + * units. For instance, `formatUnit(0.345)` will return `345 mm`. + * + * @param {number} val The value. A length or area in base units. With the + * default unit table, this is in meters. With the `areaUnitsTable`, this + * is square meters. + * @param {string|object[]} [unit='si'] The name of the unit system or a unit + * table. + * @param {object} [table=unitTable] The table of the unit system. Ignored if + * `unit` is a unit table. + * @param {number} [digits=3] The minimum number of significant figures. + * @returns {string} A formatted string or `undefined`. + */ +scaleWidget.formatUnit = function (val, unit, table, digits) { + if (val === undefined || val === null) { + return; + } + if (!Array.isArray(unit)) { + table = table || scaleWidget.unitsTable; + if (!table || !table[unit || 'si']) { + return; + } + unit = table[unit || 'si']; + } + let pos; + for (pos = 0; pos < unit.length - 1; pos += 1) { + if (val < unit[pos + 1].scale) { + break; + } + } + unit = unit[pos]; + val /= unit.scale; + digits = Math.max(0, -Math.ceil(Math.log10(val)) + (digits === undefined || digits < 0 ? 3 : digits)); + if (digits > 10) { + return; + } + let result = val.toFixed(digits); + if (digits) { + while (result.substr(result.length - 1) === '0') { + result = result.substr(0, result.length - 1); + } + if (result.substr(result.length - 1) === '.') { + result = result.substr(0, result.length - 1); + } + } + return result + ' ' + unit.unit; +}; + registerWidget('dom', 'scale', scaleWidget); module.exports = scaleWidget; diff --git a/src/util/color.js b/src/util/color.js index 6ca6653d83..ba9c2789c3 100644 --- a/src/util/color.js +++ b/src/util/color.js @@ -267,7 +267,7 @@ var colorUtils = { * standard, `rgb` and `rgba` are aliases of each other, as are `hsl` and * `hsla`. * @name cssColorConversions - * @property cssColorConversions + * @property cssColorConversions {geo.util.cssColorConversionRecord[]} * @memberof geo.util */ colorUtils.cssColorConversions = [{ diff --git a/tests/cases/scaleWidget.js b/tests/cases/scaleWidget.js index c1c15a6442..5e36cca742 100644 --- a/tests/cases/scaleWidget.js +++ b/tests/cases/scaleWidget.js @@ -56,7 +56,7 @@ describe('geo.gui.scaleWidget', function () { expect(result.pixels).toBe(150); result = widget._scaleValue(1.6e-5, 200); expect(result.value).toBe(1.5e-5); - expect(result.html).toBe('15 μm'); + expect(result.html).toBe('15 \u03BCm'); expect(widget._scaleValue(1, 200, 'miles').html).toBe('3 ft'); expect(widget._scaleValue(0.2, 200, 'miles').html).toBe('6 in'); expect(widget._scaleValue(0.01, 200, 'miles').html).toBe('0.3 in'); @@ -132,4 +132,28 @@ describe('geo.gui.scaleWidget', function () { expect(widget.options('orientation')).toBe('right'); }); }); + + describe('Check class utility methods', function () { + it('formatUnit', function () { + var formatUnit = geo.gui.scaleWidget.formatUnit, + areaUnitsTable = geo.gui.scaleWidget.areaUnitsTable; + expect(formatUnit(3, 'si')).toBe('3 m'); + expect(formatUnit(3e-4, 'si')).toBe('300 \u03BCm'); + expect(formatUnit(3.4567, 'si')).toBe('3.46 m'); + expect(formatUnit(3.4567)).toBe('3.46 m'); + expect(formatUnit('3.4567', 'si')).toBe('3.46 m'); + expect(formatUnit(3.4567, 'si', undefined, 2)).toBe('3.5 m'); + expect(formatUnit(3.4567, 'si', undefined, 0)).toBe('3 m'); + expect(formatUnit(3.4567, 'si', undefined, -2)).toBe('3.46 m'); + expect(formatUnit(34567, 'si')).toBe('34.6 km'); + expect(formatUnit(3.4567, 'si', areaUnitsTable)).toBe('3.46 m\xB2'); + expect(formatUnit(null, 'si')).toBe(undefined); + expect(formatUnit(3e-100, 'si')).toBe(undefined); + expect(formatUnit(3.4567, 'nosuchunits')).toBe(undefined); + expect(formatUnit(3.4567, 'miles')).toBe('11.3 ft'); + expect(formatUnit(3.4567e-3, 'miles')).toBe('0.136 in'); + expect(formatUnit(3.4567e3, 'miles')).toBe('2.15 mi'); + expect(formatUnit(3.4567e3, [{unit: 'NM', scale: 1852}])).toBe('1.87 NM'); + }); + }); });