diff --git a/modules/ui/info/measurement.js b/modules/ui/info/measurement.js new file mode 100644 index 000000000..74e2ceecd --- /dev/null +++ b/modules/ui/info/measurement.js @@ -0,0 +1,253 @@ +import * as d3 from 'd3'; +import _ from 'lodash'; +import { d3keybinding } from '../lib/d3.keybinding.js'; +import { t } from '../util/locale'; +import { geoExtent } from '../geo/index'; +import { utilDetect } from '../util/detect'; +import { uiCmd } from './cmd'; + +import { + geoLength as d3GeoLength, + geoCentroid as d3GeoCentroid +} from 'd3'; + + +export function uiInfoMeasurement(context) { + var isImperial = (utilDetect().locale.toLowerCase() === 'en-us'), + isHidden = true; + + + function info(selection) { + + function radiansToMeters(r) { + // using WGS84 authalic radius (6371007.1809 m) + return r * 6371007.1809; + } + + function steradiansToSqmeters(r) { + // http://gis.stackexchange.com/a/124857/40446 + return r / (4 * Math.PI) * 510065621724000; + } + + + function toLineString(feature) { + if (feature.type === 'LineString') return feature; + + var result = { type: 'LineString', coordinates: [] }; + if (feature.type === 'Polygon') { + result.coordinates = feature.coordinates[0]; + } else if (feature.type === 'MultiPolygon') { + result.coordinates = feature.coordinates[0][0]; + } + + return result; + } + + + 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() { + if (isHidden) return; + + var resolver = context.graph(), + selected = _.filter(context.selectedIDs(), function(e) { return context.hasEntity(e); }), + singular = selected.length === 1 ? selected[0] : null, + extent = geoExtent(), + entity; + + wrap.html(''); + wrap.append('h4') + .attr('class', 'infobox-heading fillD') + .text(singular || t('infobox.selected', { n: selected.length })); + + if (!selected.length) return; + + var center; + for (var i = 0; i < selected.length; i++) { + entity = context.entity(selected[i]); + extent._extend(entity.extent(resolver)); + } + center = extent.center(); + + + var list = wrap.append('ul'); + + // multiple features, just display extent center.. + if (!singular) { + list.append('li') + .text(t('infobox.center') + ': ' + center[0].toFixed(5) + ', ' + center[1].toFixed(5)); + return; + } + + // single feature, display details.. + if (!entity) return; + var geometry = entity.geometry(resolver); + + if (geometry === 'line' || geometry === 'area') { + var closed = (entity.type === 'relation') || (entity.isClosed() && !entity.isDegenerate()), + feature = entity.asGeoJSON(resolver), + length = radiansToMeters(d3GeoLength(toLineString(feature))), + lengthLabel = t('infobox.' + (closed ? 'perimeter' : 'length')), + centroid = d3GeoCentroid(feature); + + list.append('li') + .text(t('infobox.geometry') + ': ' + + (closed ? t('infobox.closed') + ' ' : '') + t('geometry.' + geometry) ); + + if (closed) { + var area = steradiansToSqmeters(entity.area(resolver)); + list.append('li') + .text(t('infobox.area') + ': ' + displayArea(area)); + } + + list.append('li') + .text(lengthLabel + ': ' + displayLength(length)); + + list.append('li') + .text(t('infobox.centroid') + ': ' + centroid[0].toFixed(5) + ', ' + centroid[1].toFixed(5)); + + + var toggle = isImperial ? 'imperial' : 'metric'; + wrap.append('a') + .text(t('infobox.' + toggle)) + .attr('href', '#') + .attr('class', 'button') + .on('click', function() { + d3.event.preventDefault(); + isImperial = !isImperial; + redraw(); + }); + + } else { + var centerLabel = t('infobox.' + (entity.type === 'node' ? 'location' : 'center')); + + list.append('li') + .text(t('infobox.geometry') + ': ' + t('geometry.' + geometry)); + + list.append('li') + .text(centerLabel + ': ' + center[0].toFixed(5) + ', ' + center[1].toFixed(5)); + } + } + + + function toggle() { + if (d3.event) { + d3.event.preventDefault(); + } + + isHidden = !isHidden; + + if (isHidden) { + wrap + .style('display', 'block') + .style('opacity', 1) + .transition() + .duration(200) + .style('opacity', 0) + .on('end', function() { + d3.select(this).style('display', 'none'); + }); + } else { + wrap + .style('display', 'block') + .style('opacity', 0) + .transition() + .duration(200) + .style('opacity', 1) + .on('end', function() { + redraw(); + }); + } + } + + + var wrap = selection.selectAll('.infobox') + .data([0]); + + wrap = wrap.enter() + .append('div') + .attr('class', 'infobox fillD2') + .style('display', (isHidden ? 'none' : 'block')) + .merge(wrap); + + context.map() + .on('drawn.info', redraw); + + redraw(); + + var keybinding = d3keybinding('info') + .on(uiCmd('⌘' + t('infobox.key')), toggle); + + d3.select(document) + .call(keybinding); + } + + return info; +}