From 6f5d68d6ed98ffbccb7642372b5c8d5d86401d99 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 30 Apr 2024 19:17:28 -0400 Subject: [PATCH 01/29] Expand Map JS API --- .../app/js/classes/plugins/ApiFactory.js | 27 ++++++++++++++----- app/static/app/js/classes/plugins/Map.js | 6 ++++- app/static/app/js/components/Map.jsx | 5 +++- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/static/app/js/classes/plugins/ApiFactory.js b/app/static/app/js/classes/plugins/ApiFactory.js index d13531039..2f1c20293 100644 --- a/app/static/app/js/classes/plugins/ApiFactory.js +++ b/app/static/app/js/classes/plugins/ApiFactory.js @@ -15,13 +15,6 @@ export default class ApiFactory{ // are more robust as we can detect more easily if // things break - // TODO: we should consider refactoring this code - // to use functions instead of events. Originally - // we chose to use events because that would have - // decreased coupling, but since all API pubsub activity - // evolved to require a call to the PluginsAPI object, we might have - // added a bunch of complexity for no real advantage here. - const addEndpoint = (obj, eventName, preTrigger = () => {}) => { const emitResponse = response => { // Timeout needed for modules that have no dependencies @@ -99,6 +92,26 @@ export default class ApiFactory{ obj = Object.assign(obj, api.helpers); } + // Handle syncronous function on/off/export + (api.functions || []).forEach(func => { + let callbacks = []; + obj[func] = (...args) => { + for (let i = 0; i < callbacks.length; i++){ + if ((callbacks[i])(...args)) return true; + } + return false; + }; + + const onName = "on" + func[0].toUpperCase() + func.slice(1); + const offName = "off" + func[0].toUpperCase() + func.slice(1); + obj[onName] = f => { + callbacks.push(f); + }; + obj[offName] = f => { + callbacks = callbacks.filter(cb => cb !== f); + }; + }); + return obj; } diff --git a/app/static/app/js/classes/plugins/Map.js b/app/static/app/js/classes/plugins/Map.js index d5abc807f..f7aae890f 100644 --- a/app/static/app/js/classes/plugins/Map.js +++ b/app/static/app/js/classes/plugins/Map.js @@ -19,7 +19,11 @@ export default { endpoints: [ ["willAddControls", leafletPreCheck], ["didAddControls", layersControlPreCheck], - ["addActionButton", leafletPreCheck], + ["addActionButton", leafletPreCheck] + ], + + functions: [ + "handleClick" ] }; diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index a807c4585..9e603e8e6 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -397,7 +397,8 @@ class Map extends React.Component { PluginsAPI.Map.triggerWillAddControls({ map: this.map, - tiles + tiles, + mapView: this }); let scaleControl = Leaflet.control.scale({ @@ -524,6 +525,8 @@ _('Example:'), this.map.fitBounds(this.mapBounds); this.map.on('click', e => { + if (PluginsAPI.Map.handleClick(e)) return; + // Find first tile layer at the selected coordinates for (let layer of this.state.imageryLayers){ if (layer._map && layer.options.bounds.contains(e.latlng)){ From a44c2ce86f2574b4cdb40b723360ee2b8e3bfa70 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 1 May 2024 12:34:47 -0400 Subject: [PATCH 02/29] Add isMobile function --- app/static/app/js/classes/Utils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/static/app/js/classes/Utils.js b/app/static/app/js/classes/Utils.js index e843275d0..cb449647b 100644 --- a/app/static/app/js/classes/Utils.js +++ b/app/static/app/js/classes/Utils.js @@ -103,6 +103,10 @@ export default { var sizes = ['bytes', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb', 'Zb', 'Yb']; var i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + }, + + isMobile: function(){ + return navigator.userAgent.match(/(iPad)|(iPhone)|(iPod)|(android)|(webOS)/i); } }; From e7337f3b5daeff1c9de5917bc8f4b76d69a15356 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 1 May 2024 13:39:10 -0400 Subject: [PATCH 03/29] Silence annoying React deprecation notice of useful functionality --- app/static/app/js/main.jsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/static/app/js/main.jsx b/app/static/app/js/main.jsx index 445205a67..3c219ef38 100644 --- a/app/static/app/js/main.jsx +++ b/app/static/app/js/main.jsx @@ -8,6 +8,16 @@ import { setLocale } from './translations/functions'; // Main is always executed first in the page +// Silence annoying React deprecation notice of useful functionality +const originalError = console.error; +console.error = function(...args) { + let message = args[0]; + if (typeof message === 'string' && message.indexOf('Warning: A future version of React will block javascript:') !== -1) { + return; + } + originalError.apply(console, args); +}; + // We share some objects to avoid having to include them // as a dependency in each component (adds too much space overhead) window.ReactDOM = ReactDOM; From 2352d838cfaabc70e2dbb8d6e05ff42781729782 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 1 May 2024 14:01:40 -0400 Subject: [PATCH 04/29] Shorten Lightning Network --> Lightning --- coreplugins/lightning/manifest.json | 4 ++-- coreplugins/lightning/plugin.py | 4 ++-- coreplugins/lightning/public/app.jsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/coreplugins/lightning/manifest.json b/coreplugins/lightning/manifest.json index b86e45137..463200507 100644 --- a/coreplugins/lightning/manifest.json +++ b/coreplugins/lightning/manifest.json @@ -1,7 +1,7 @@ { - "name": "Lightning Network Bridge", + "name": "Lightning", "webodmMinVersion": "0.7.1", - "description": "Sync accounts from webodm.net", + "description": "Process in the cloud with webodm.net", "version": "0.9.0", "author": "Piero Toffanin", "email": "pt@masseranolabs.com", diff --git a/coreplugins/lightning/plugin.py b/coreplugins/lightning/plugin.py index 9cd68e0a7..4a7597c2e 100644 --- a/coreplugins/lightning/plugin.py +++ b/coreplugins/lightning/plugin.py @@ -22,7 +22,7 @@ def JsonResponse(dict): class Plugin(PluginBase): def main_menu(self): - return [Menu(_("Lightning Network"), self.public_url(""), "fa fa-bolt fa-fw")] + return [Menu(_("Lightning"), self.public_url(""), "fa fa-bolt fa-fw")] def include_js_files(self): return ['add_cost_estimate.js'] @@ -36,7 +36,7 @@ def main(request): uds = UserDataStore('lightning', request.user) return render(request, self.template_path("index.html"), { - 'title': _('Lightning Network'), + 'title': _('Lightning'), 'api_key': uds.get_string("api_key") }) diff --git a/coreplugins/lightning/public/app.jsx b/coreplugins/lightning/public/app.jsx index 1f6024267..718b47f1e 100644 --- a/coreplugins/lightning/public/app.jsx +++ b/coreplugins/lightning/public/app.jsx @@ -36,7 +36,7 @@ export default class LightningPanel extends React.Component { return (
{ !apiKey ?
-

{_("Lightning Network")}

+

{_("Lightning")}

{_("Lightning is a service that allows you to quickly process small and large datasets using high performance servers in the cloud.")} webodm.net', register: `${_("register")}`}}> {_("Below you can enter your %(link)s credentials to sync your account and automatically setup a new processing node. If you don't have an account, you can %(register)s for free.")} From 972b06d03b43c7a7d85d4acfb3f14e845ca44306 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 1 May 2024 15:43:32 -0400 Subject: [PATCH 05/29] Fix triangle icon, bump version --- app/static/app/js/components/TaskListItem.jsx | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/static/app/js/components/TaskListItem.jsx b/app/static/app/js/components/TaskListItem.jsx index 2ea14f77a..f895582a0 100644 --- a/app/static/app/js/components/TaskListItem.jsx +++ b/app/static/app/js/components/TaskListItem.jsx @@ -606,7 +606,7 @@ class TaskListItem extends React.Component { /> : ""} {showOrthophotoMissingWarning ? -
{_("An orthophoto could not be generated. To generate one, make sure GPS information is embedded in the EXIF tags of your images, or use a Ground Control Points (GCP) file.")}
: ""} +
{_("An orthophoto could not be generated. To generate one, make sure GPS information is embedded in the EXIF tags of your images, or use a Ground Control Points (GCP) file.")}
: ""} {showMemoryErrorWarning ?
${_("enough RAM allocated")}`, cloudlink: `${_("cloud processing node")}` }}>{_("It looks like your processing node ran out of memory. If you are using docker, make sure that your docker environment has %(memlink)s. Alternatively, make sure you have enough physical RAM, reduce the number of images, make your images smaller, or reduce the max-concurrency parameter from the task's options. You can also try to use a %(cloudlink)s.")}
: ""} diff --git a/package.json b/package.json index 579a31c93..4d78f5493 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "2.4.2", + "version": "2.4.3", "description": "User-friendly, extendable application and API for processing aerial imagery.", "main": "index.js", "scripts": { From 59e104946c21a5e939f5f33d1da58f41c8a169fe Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 2 May 2024 16:08:41 -0400 Subject: [PATCH 06/29] Better error message on worker failure --- app/static/app/js/classes/Workers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/app/js/classes/Workers.js b/app/static/app/js/classes/Workers.js index 29bd19940..42a761bf3 100644 --- a/app/static/app/js/classes/Workers.js +++ b/app/static/app/js/classes/Workers.js @@ -21,7 +21,7 @@ export default { }).fail(error => { console.warn(error); if (errorCount++ < 10) setTimeout(() => check(), 2000); - else cb(JSON.stringify(error)); + else cb(error.statusText); }); }; From d73558256a0fca651a3432ae5855a63a073930f1 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 2 May 2024 16:17:06 -0400 Subject: [PATCH 07/29] Cleaner map controls, fix opacity label alignment --- app/static/app/js/components/Map.jsx | 11 +---- app/static/app/js/css/Map.scss | 5 ++ .../leaflet/L.Control.MousePosition.css | 9 ---- .../vendor/leaflet/L.Control.MousePosition.js | 48 ------------------- 4 files changed, 7 insertions(+), 66 deletions(-) delete mode 100644 app/static/app/js/vendor/leaflet/L.Control.MousePosition.css delete mode 100644 app/static/app/js/vendor/leaflet/L.Control.MousePosition.js diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index 9e603e8e6..6b988deb7 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -4,8 +4,6 @@ import '../css/Map.scss'; import 'leaflet/dist/leaflet.css'; import Leaflet from 'leaflet'; import async from 'async'; -import '../vendor/leaflet/L.Control.MousePosition.css'; -import '../vendor/leaflet/L.Control.MousePosition'; import '../vendor/leaflet/Leaflet.Autolayers/css/leaflet.auto-layers.css'; import '../vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers'; // import '../vendor/leaflet/L.TileLayer.NoGap'; @@ -385,7 +383,7 @@ class Map extends React.Component { this.map = Leaflet.map(this.container, { scrollWheelZoom: true, - positionControl: true, + positionControl: false, zoomControl: false, minZoom: 0, maxZoom: 24 @@ -401,10 +399,6 @@ class Map extends React.Component { mapView: this }); - let scaleControl = Leaflet.control.scale({ - maxWidth: 250, - }).addTo(this.map); - //add zoom control with your options let zoomControl = Leaflet.control.zoom({ position:'bottomleft' @@ -580,7 +574,6 @@ _('Example:'), tiles: tiles, controls:{ autolayers: this.autolayers, - scale: scaleControl, zoom: zoomControl } }); @@ -630,7 +623,7 @@ _('Example:'),
- {_("Opacity:")} +
{_("Opacity:")}
Date: Thu, 2 May 2024 16:47:47 -0400 Subject: [PATCH 08/29] Started adding app-wide unit selector logic --- app/static/app/js/classes/Units.js | 69 +++++++++++++++++++ app/static/app/js/components/UnitSelector.jsx | 30 ++++++++ .../js/components/tests/UnitSelector.test.jsx | 10 +++ app/static/app/js/main.jsx | 8 +++ 4 files changed, 117 insertions(+) create mode 100644 app/static/app/js/classes/Units.js create mode 100644 app/static/app/js/components/UnitSelector.jsx create mode 100644 app/static/app/js/components/tests/UnitSelector.test.jsx diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js new file mode 100644 index 000000000..71dffce84 --- /dev/null +++ b/app/static/app/js/classes/Units.js @@ -0,0 +1,69 @@ +import { _ } from '../classes/gettext'; + +const units = { + acres: { + factor: 0.00024711, + label: _('Acres'), + abbr: 'ac' + }, + feet: { + factor: 3.2808, + label: _('Feet'), + abbr: 'ft' + }, + hectares: { + factor: 0.0001, + label: _('Hectares'), + abbr: 'ha' + }, + meters: { + factor: 1, + label: _('Meters'), + abbr: 'm' + }, + kilometers: { + factor: 0.001, + label: _('Kilometers'), + abbr: 'km' + }, + centimeters: { + factor: 100, + label: _('Centimeters'), + abbr: 'cm' + }, + miles: { + factor: 3.2808 / 5280, + label: _('Miles'), + abbr: 'mi' + }, + sqfeet: { + factor: 10.7639, + label: _('Square Feet'), + abbr: 'ft²' + }, + sqmeters: { + factor: 1, + label: _('Square Meters'), + abbr: 'm²' + }, + sqmiles: { + factor: 0.000000386102, + label: _('Square Miles'), + abbr: 'mi²' + } + }; + +const systems = { + metric: { + length: [units.kilometers, units.meters, units.centimeters], + area: [units.sqmeters] + } + + // TODO +} + +export default { + // to be used on individual strings + +}; + diff --git a/app/static/app/js/components/UnitSelector.jsx b/app/static/app/js/components/UnitSelector.jsx new file mode 100644 index 000000000..f754e9dde --- /dev/null +++ b/app/static/app/js/components/UnitSelector.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class UnitSelector extends React.Component { + static propTypes = { + } + + constructor(props){ + super(props); + + this.state = { + system: window.getPreferredUnitSystem() + } + } + + handleChange = e => { + this.setState({system: e.target.value}); + window.setPreferredUnitSystem(e.target.value); + }; + + render() { + return ( + + ); + } +} + +export default UnitSelector; diff --git a/app/static/app/js/components/tests/UnitSelector.test.jsx b/app/static/app/js/components/tests/UnitSelector.test.jsx new file mode 100644 index 000000000..8d64b46d7 --- /dev/null +++ b/app/static/app/js/components/tests/UnitSelector.test.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import UnitSelector from '../UnitSelector'; + +describe('', () => { + it('renders without exploding', () => { + const wrapper = shallow(); + expect(wrapper.exists()).toBe(true); + }) +}); \ No newline at end of file diff --git a/app/static/app/js/main.jsx b/app/static/app/js/main.jsx index 3c219ef38..8b0b86923 100644 --- a/app/static/app/js/main.jsx +++ b/app/static/app/js/main.jsx @@ -26,6 +26,14 @@ window.React = React; // Expose set locale function globally window.setLocale = setLocale; +// Expose to allow every part of the app to access this information +window.getPreferredUnitSystem = () => { + return localStorage.getItem("preferred_unit_system") || "metric"; +}; +window.setPreferredUnitSystem = (system) => { + localStorage.setItem("preferred_unit_system", system); +}; + $(function(){ PluginsAPI.App.triggerReady(); }); From 30eff78d3bfd95562ef42c8f1d2f870493edf91e Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 7 May 2024 11:53:21 -0400 Subject: [PATCH 09/29] Units work --- app/api/tasks.py | 6 +++ app/static/app/js/classes/Units.js | 82 +++++++++++++++++++++++++----- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/app/api/tasks.py b/app/api/tasks.py index 9cb56c4b4..02a0e4e3e 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -204,6 +204,12 @@ def upload(self, request, pk=None, project_pk=None): raise exceptions.NotFound() files = flatten_files(request.FILES) + + import time + import random + for f in files: + if f.name == 'DJI_0018.JPG': + return Response("Timeout", status=status.HTTP_504_GATEWAY_TIMEOUT) if len(files) == 0: raise exceptions.ValidationError(detail=_("No files uploaded")) diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js index 71dffce84..c546ee394 100644 --- a/app/static/app/js/classes/Units.js +++ b/app/static/app/js/classes/Units.js @@ -4,64 +4,118 @@ const units = { acres: { factor: 0.00024711, label: _('Acres'), - abbr: 'ac' + abbr: 'ac', + round: 5 }, feet: { factor: 3.2808, label: _('Feet'), - abbr: 'ft' + abbr: 'ft', + round: 4 }, hectares: { factor: 0.0001, label: _('Hectares'), - abbr: 'ha' + abbr: 'ha', + round: 4 }, meters: { factor: 1, label: _('Meters'), - abbr: 'm' + abbr: 'm', + round: 3 }, kilometers: { factor: 0.001, label: _('Kilometers'), - abbr: 'km' + abbr: 'km', + round: 5 }, centimeters: { factor: 100, label: _('Centimeters'), - abbr: 'cm' + abbr: 'cm', + round: 1 }, miles: { factor: 3.2808 / 5280, label: _('Miles'), - abbr: 'mi' + abbr: 'mi', + round: 5 }, sqfeet: { factor: 10.7639, label: _('Square Feet'), - abbr: 'ft²' + abbr: 'ft²', + round: 2, }, sqmeters: { factor: 1, label: _('Square Meters'), - abbr: 'm²' + abbr: 'm²', + round: 2, }, sqmiles: { factor: 0.000000386102, label: _('Square Miles'), - abbr: 'mi²' + abbr: 'mi²', + round: 5 } }; -const systems = { - metric: { - length: [units.kilometers, units.meters, units.centimeters], - area: [units.sqmeters] +class UnitSystem{ + lengthUnit(meters){ throw new Error("Not implemented"); } + areaUnit(sqmeters){ throw new Error("Not implemented"); } + + area(meters){ + } + length(sqmeters){ + const unit = this.lengthUnit(sqmeters); + const v = unit.factor * sqmeters; + return {v, s: `{v.toLocaleString()}` }; + } +}; + +class Metric extends UnitSystem{ + lengthUnit(meters){ + if (meters < 100) return units.centimeters; + else if (meters >= 1000) return units.kilometers; + else return units.meters; + } + + areaUnit(sqmeters){ + return units.sqmeters; // TODO + } +} + +class Imperial extends UnitSystem{ + lengthUnit(meters){ + const feet = units.feet.factor * meters; + if (feet >= 5280) return units.miles; + else return units.feet; + } + + areaUnit(sqmeters){ + const sqfeet = units.sqfeet.factor * meters; + if (sqfeet >= 43560 && sqfeet < 27878400) return units.acres; + else if (sqfeet >= 27878400) return units.sqmiles; + else return units.sqfeet; + } +} + +const systems = { + metric: new Metric(), + + // TODO } +let a = 100; +let S = systems.metric; + + export default { // to be used on individual strings From 9a8013d6ced19f1b01408347892d7617881a98f1 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 7 May 2024 11:56:33 -0400 Subject: [PATCH 10/29] Revert debug commit --- app/api/tasks.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/api/tasks.py b/app/api/tasks.py index 02a0e4e3e..2d59e2444 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -204,13 +204,6 @@ def upload(self, request, pk=None, project_pk=None): raise exceptions.NotFound() files = flatten_files(request.FILES) - - import time - import random - for f in files: - if f.name == 'DJI_0018.JPG': - return Response("Timeout", status=status.HTTP_504_GATEWAY_TIMEOUT) - if len(files) == 0: raise exceptions.ValidationError(detail=_("No files uploaded")) From e0eb7cad7e2a2f5d3ad81cbd69e5e0a05d796c73 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 8 May 2024 10:48:29 -0400 Subject: [PATCH 11/29] Add units tests --- app/static/app/js/classes/Units.js | 91 ++++++++++++++----- app/static/app/js/components/Map.jsx | 16 ++++ app/static/app/js/components/UnitSelector.jsx | 10 +- .../app/js/components/tests/Units.test.jsx | 41 +++++++++ app/static/app/js/main.jsx | 8 -- 5 files changed, 130 insertions(+), 36 deletions(-) create mode 100644 app/static/app/js/components/tests/Units.test.jsx diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js index c546ee394..b9fb2d9db 100644 --- a/app/static/app/js/classes/Units.js +++ b/app/static/app/js/classes/Units.js @@ -1,4 +1,4 @@ -import { _ } from '../classes/gettext'; +import { _ } from './gettext'; const units = { acres: { @@ -47,13 +47,19 @@ const units = { factor: 10.7639, label: _('Square Feet'), abbr: 'ft²', - round: 2, + round: 2 }, sqmeters: { factor: 1, label: _('Square Meters'), abbr: 'm²', - round: 2, + round: 2 + }, + sqmeters: { + factor: 0.000001, + label: _('Square Kilometers'), + abbr: 'km²', + round: 5 }, sqmiles: { factor: 0.000000386102, @@ -61,36 +67,68 @@ const units = { abbr: 'mi²', round: 5 } - }; +}; + +class ValueUnit{ + constructor(val, unit){ + this.val = val; + this.unit = unit; + } + + toString(){ + const mul = Math.pow(10, this.unit.round); + const rounded = (Math.round(this.val * mul) / mul).toString(); + + let withCommas = ""; + let parts = rounded.split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + withCommas = parts.join("."); + + return `${withCommas} ${this.unit.abbr}`; + } +} class UnitSystem{ lengthUnit(meters){ throw new Error("Not implemented"); } areaUnit(sqmeters){ throw new Error("Not implemented"); } + getName(){ throw new Error("Not implemented"); } - area(meters){ - + area(sqmeters){ + const unit = this.areaUnit(sqmeters); + const val = unit.factor * sqmeters; + return new ValueUnit(val, unit); } - length(sqmeters){ - const unit = this.lengthUnit(sqmeters); - const v = unit.factor * sqmeters; - return {v, s: `{v.toLocaleString()}` }; + length(meters){ + const unit = this.lengthUnit(meters); + const val = unit.factor * meters; + return new ValueUnit(val, unit); } }; -class Metric extends UnitSystem{ +class MetricSystem extends UnitSystem{ + getName(){ + return _("Metric"); + } + lengthUnit(meters){ - if (meters < 100) return units.centimeters; + if (meters < 1) return units.centimeters; else if (meters >= 1000) return units.kilometers; else return units.meters; } areaUnit(sqmeters){ - return units.sqmeters; // TODO + if (sqmeters >= 10000 && sqmeters < 1000000) return units.hectares; + else if (sqmeters >= 1000000) return units.sqkilometers; + return units.sqmeters; } } -class Imperial extends UnitSystem{ +class ImperialSystem extends UnitSystem{ + getName(){ + return _("Imperial"); + } + lengthUnit(meters){ const feet = units.feet.factor * meters; if (feet >= 5280) return units.miles; @@ -98,7 +136,7 @@ class Imperial extends UnitSystem{ } areaUnit(sqmeters){ - const sqfeet = units.sqfeet.factor * meters; + const sqfeet = units.sqfeet.factor * sqmeters; if (sqfeet >= 43560 && sqfeet < 27878400) return units.acres; else if (sqfeet >= 27878400) return units.sqmiles; else return units.sqfeet; @@ -106,18 +144,21 @@ class Imperial extends UnitSystem{ } const systems = { - metric: new Metric(), - - - // TODO + metric: new MetricSystem(), + imperial: new ImperialSystem() } -let a = 100; -let S = systems.metric; - +// Expose to allow every part of the app to access this information +function getPreferredUnitSystem(){ + return localStorage.getItem("preferred_unit_system") || "metric"; +} +function setPreferredUnitSystem(system){ + localStorage.setItem("preferred_unit_system", system); +} -export default { - // to be used on individual strings - +export { + systems, + getPreferredUnitSystem, + setPreferredUnitSystem }; diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index 6b988deb7..b04274fd8 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -27,6 +27,7 @@ import '../vendor/leaflet/Leaflet.Ajax'; import 'rbush'; import '../vendor/leaflet/leaflet-markers-canvas'; import { _ } from '../classes/gettext'; +import UnitSelector from './UnitSelector'; class Map extends React.Component { static defaultProps = { @@ -404,6 +405,21 @@ class Map extends React.Component { position:'bottomleft' }).addTo(this.map); + const UnitsCtrl = Leaflet.Control.extend({ + options: { + position: 'bottomleft' + }, + + onAdd: function () { + this.container = Leaflet.DomUtil.create('div', 'leaflet-control-units-selection leaflet-control'); + Leaflet.DomEvent.disableClickPropagation(this.container); + ReactDOM.render(, this.container); + return this.container; + } + }); + new UnitsCtrl().addTo(this.map); + + if (showBackground) { this.basemaps = {}; diff --git a/app/static/app/js/components/UnitSelector.jsx b/app/static/app/js/components/UnitSelector.jsx index f754e9dde..37361de17 100644 --- a/app/static/app/js/components/UnitSelector.jsx +++ b/app/static/app/js/components/UnitSelector.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { systems, getPreferredUnitSystem, setPreferredUnitSystem } from '../classes/Units'; class UnitSelector extends React.Component { static propTypes = { @@ -9,19 +10,22 @@ class UnitSelector extends React.Component { super(props); this.state = { - system: window.getPreferredUnitSystem() + system: getPreferredUnitSystem() } + + // console.log(systems.metric.length(1.01).toString()); } handleChange = e => { this.setState({system: e.target.value}); - window.setPreferredUnitSystem(e.target.value); + setPreferredUnitSystem(e.target.value); }; render() { return ( ); } diff --git a/app/static/app/js/components/tests/Units.test.jsx b/app/static/app/js/components/tests/Units.test.jsx new file mode 100644 index 000000000..dd1929ab3 --- /dev/null +++ b/app/static/app/js/components/tests/Units.test.jsx @@ -0,0 +1,41 @@ +import { systems } from '../../classes/Units'; + +describe('Metric system', () => { + it('it should display units properly', () => { + + const { metric } = systems; + + const lengths = [ + [1, "1 m"], + [0.01, "1 cm"], + [0.0154, "1.5 cm"], + [0.99, "99 cm"], + [0.995555, "99.6 cm"], + [1.01, "1.01 m"], + [999, "999 m"], + [1000, "1 km"], + [1001, "1.001 km"], + [1000010, "1,000.01 km"], + [1000012.349, "1,000.01235 km"], + ]; + + lengths.forEach(l => { + expect(metric.length(l[0]).toString()).toBe(l[1]); + }); + + const areas = [ + [1, "1 m²"], + [9999, "9,999 m²"], + [10000, "1 ha"], + [11005, "1.1005 ha"], + [11005, "1.1005 ha"], + [999999, "99.9999 ha"], + [1000000, "1 km²"], + [1000000000, "1,000 km²"] + ]; + + areas.forEach(a => { + expect(metric.area(a[0]).toString()).toBe(a[1]); + }); + }) +}); \ No newline at end of file diff --git a/app/static/app/js/main.jsx b/app/static/app/js/main.jsx index 8b0b86923..3c219ef38 100644 --- a/app/static/app/js/main.jsx +++ b/app/static/app/js/main.jsx @@ -26,14 +26,6 @@ window.React = React; // Expose set locale function globally window.setLocale = setLocale; -// Expose to allow every part of the app to access this information -window.getPreferredUnitSystem = () => { - return localStorage.getItem("preferred_unit_system") || "metric"; -}; -window.setPreferredUnitSystem = (system) => { - localStorage.setItem("preferred_unit_system", system); -}; - $(function(){ PluginsAPI.App.triggerReady(); }); From ece6bba20010ff248e666e3713350bdb45512557 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 9 May 2024 09:27:10 -0400 Subject: [PATCH 12/29] Moar unit tests --- app/static/app/js/classes/Units.js | 21 +++-------- .../app/js/components/tests/Units.test.jsx | 35 ++++++++++++++++++- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js index b9fb2d9db..4b36e36f9 100644 --- a/app/static/app/js/classes/Units.js +++ b/app/static/app/js/classes/Units.js @@ -2,68 +2,57 @@ import { _ } from './gettext'; const units = { acres: { - factor: 0.00024711, - label: _('Acres'), + factor: 1 / 4046.85642, abbr: 'ac', round: 5 }, feet: { - factor: 3.2808, - label: _('Feet'), + factor: 3.28084, abbr: 'ft', round: 4 }, hectares: { factor: 0.0001, - label: _('Hectares'), abbr: 'ha', round: 4 }, meters: { factor: 1, - label: _('Meters'), abbr: 'm', round: 3 }, kilometers: { factor: 0.001, - label: _('Kilometers'), abbr: 'km', round: 5 }, centimeters: { factor: 100, - label: _('Centimeters'), abbr: 'cm', round: 1 }, miles: { - factor: 3.2808 / 5280, - label: _('Miles'), + factor: 3.28084 / 5280, abbr: 'mi', round: 5 }, sqfeet: { - factor: 10.7639, - label: _('Square Feet'), + factor: 1 / 0.09290304, abbr: 'ft²', round: 2 }, sqmeters: { factor: 1, - label: _('Square Meters'), abbr: 'm²', round: 2 }, - sqmeters: { + sqkilometers: { factor: 0.000001, - label: _('Square Kilometers'), abbr: 'km²', round: 5 }, sqmiles: { factor: 0.000000386102, - label: _('Square Miles'), abbr: 'mi²', round: 5 } diff --git a/app/static/app/js/components/tests/Units.test.jsx b/app/static/app/js/components/tests/Units.test.jsx index dd1929ab3..f616890ed 100644 --- a/app/static/app/js/components/tests/Units.test.jsx +++ b/app/static/app/js/components/tests/Units.test.jsx @@ -31,11 +31,44 @@ describe('Metric system', () => { [11005, "1.1005 ha"], [999999, "99.9999 ha"], [1000000, "1 km²"], - [1000000000, "1,000 km²"] + [1000000000, "1,000 km²"], + [1000255558, "1,000.25556 km²"] ]; areas.forEach(a => { expect(metric.area(a[0]).toString()).toBe(a[1]); }); }) +}); + +describe('Imperial system', () => { + it('it should display units properly', () => { + + const { imperial } = systems; + + const lengths = [ + [1, "3.2808 ft"], + [0.01, "0.0328 ft"], + [0.0154, "0.0505 ft"], + [1609, "5,278.8716 ft"], + [1609.344, "1 mi"], + [3218.69, "2 mi"] + ]; + + lengths.forEach(l => { + expect(imperial.length(l[0]).toString()).toBe(l[1]); + }); + + const areas = [ + [1, "10.76 ft²"], + [9999, "2.47081 ac"], + [4046.86, "1 ac"], + [2587398.1, "639.35999 ac"], + [2.59e+6, "1 mi²"] + ]; + + areas.forEach(a => { + expect(imperial.area(a[0]).toString()).toBe(a[1]); + }); + }) }); \ No newline at end of file From adf9c7dc5ffaae35af15e88c0e22ed9c13c4520a Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 9 May 2024 11:08:51 -0400 Subject: [PATCH 13/29] Add US imperial --- app/static/app/js/classes/Units.js | 104 +++++++++++++++--- .../app/js/components/tests/Units.test.jsx | 34 +++--- 2 files changed, 107 insertions(+), 31 deletions(-) diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js index 4b36e36f9..4da1e3a7b 100644 --- a/app/static/app/js/classes/Units.js +++ b/app/static/app/js/classes/Units.js @@ -2,15 +2,25 @@ import { _ } from './gettext'; const units = { acres: { - factor: 1 / 4046.85642, - abbr: 'ac', + factor: (1 / (0.3048 * 0.3048)) / 43560, + abbr: 'ac', + round: 5 + }, + acres_us: { + factor: Math.pow(3937 / 1200, 2) / 43560, + abbr: 'ac (US)', round: 5 }, feet: { - factor: 3.28084, + factor: 1 / 0.3048, abbr: 'ft', round: 4 }, + feet_us:{ + factor: 3937 / 1200, + abbr: 'ft (US)', + round: 4 + }, hectares: { factor: 0.0001, abbr: 'ha', @@ -32,15 +42,25 @@ const units = { round: 1 }, miles: { - factor: 3.28084 / 5280, - abbr: 'mi', - round: 5 + factor: (1 / 0.3048) / 5280, + abbr: 'mi', + round: 5 + }, + miles_us: { + factor: (3937 / 1200) / 5280, + abbr: 'mi (US)', + round: 5 }, sqfeet: { - factor: 1 / 0.09290304, + factor: 1 / (0.3048 * 0.3048), abbr: 'ft²', round: 2 }, + sqfeet_us: { + factor: Math.pow(3937 / 1200, 2), + abbr: 'ft² (US)', + round: 2 + }, sqmeters: { factor: 1, abbr: 'm²', @@ -52,9 +72,14 @@ const units = { round: 5 }, sqmiles: { - factor: 0.000000386102, + factor: Math.pow((1 / 0.3048) / 5280, 2), abbr: 'mi²', round: 5 + }, + sqmiles_us: { + factor: Math.pow((3937 / 1200) / 5280, 2), + abbr: 'mi² (US)', + round: 5 } }; @@ -117,24 +142,71 @@ class ImperialSystem extends UnitSystem{ getName(){ return _("Imperial"); } + + feet(){ + return units.feet; + } + + sqfeet(){ + return units.sqfeet; + } + + miles(){ + return units.miles; + } + + sqmiles(){ + return units.sqmiles; + } + + acres(){ + return units.acres; + } lengthUnit(meters){ - const feet = units.feet.factor * meters; - if (feet >= 5280) return units.miles; - else return units.feet; + const feet = this.feet().factor * meters; + if (feet >= 5280) return this.miles(); + else return this.feet(); } areaUnit(sqmeters){ - const sqfeet = units.sqfeet.factor * sqmeters; - if (sqfeet >= 43560 && sqfeet < 27878400) return units.acres; - else if (sqfeet >= 27878400) return units.sqmiles; - else return units.sqfeet; + const sqfeet = this.sqfeet().factor * sqmeters; + if (sqfeet >= 43560 && sqfeet < 27878400) return this.acres(); + else if (sqfeet >= 27878400) return this.sqmiles(); + else return this.sqfeet(); + } +} + +class ImperialUSSystem extends ImperialSystem{ + getName(){ + return _("Imperial (US)"); + } + + feet(){ + return units.feet_us; + } + + sqfeet(){ + return units.sqfeet_us; + } + + miles(){ + return units.miles_us; + } + + sqmiles(){ + return units.sqmiles_us; + } + + acres(){ + return units.acres_us; } } const systems = { metric: new MetricSystem(), - imperial: new ImperialSystem() + imperial: new ImperialSystem(), + imperialUS: new ImperialUSSystem() } // Expose to allow every part of the app to access this information diff --git a/app/static/app/js/components/tests/Units.test.jsx b/app/static/app/js/components/tests/Units.test.jsx index f616890ed..c268ed18b 100644 --- a/app/static/app/js/components/tests/Units.test.jsx +++ b/app/static/app/js/components/tests/Units.test.jsx @@ -41,34 +41,38 @@ describe('Metric system', () => { }) }); -describe('Imperial system', () => { +describe('Imperial systems', () => { it('it should display units properly', () => { - const { imperial } = systems; + const { imperial, imperialUS } = systems; const lengths = [ - [1, "3.2808 ft"], - [0.01, "0.0328 ft"], - [0.0154, "0.0505 ft"], - [1609, "5,278.8716 ft"], - [1609.344, "1 mi"], - [3218.69, "2 mi"] + [1, "3.2808 ft", "3.2808 ft (US)"], + [0.01, "0.0328 ft", "0.0328 ft (US)"], + [0.0154, "0.0505 ft", "0.0505 ft (US)"], + [1609, "5,278.8714 ft", "5,278.8608 ft (US)"], + [1609.344, "1 mi", "5,279.9894 ft (US)"], + [1609.3472187, "1 mi", "1 mi (US)"], + [3218.69, "2 mi", "2 mi (US)"] ]; lengths.forEach(l => { - expect(imperial.length(l[0]).toString()).toBe(l[1]); + expect(imperial.length(l[0]).toString()).toBe(l[1]); + expect(imperialUS.length(l[0]).toString()).toBe(l[2]); }); const areas = [ - [1, "10.76 ft²"], - [9999, "2.47081 ac"], - [4046.86, "1 ac"], - [2587398.1, "639.35999 ac"], - [2.59e+6, "1 mi²"] + [1, "10.76 ft²", "10.76 ft² (US)"], + [9999, "2.47081 ac", "2.4708 ac (US)"], + [4046.86, "1 ac", "43,559.86 ft² (US)"], + [4046.87261, "1 ac", "1 ac (US)"], + [2587398.1, "639.35999 ac", "639.35744 ac (US)"], + [2.59e+6, "1 mi²", "1 mi² (US)"] ]; areas.forEach(a => { - expect(imperial.area(a[0]).toString()).toBe(a[1]); + expect(imperial.area(a[0]).toString()).toBe(a[1]); + expect(imperialUS.area(a[0]).toString()).toBe(a[2]); }); }) }); \ No newline at end of file From 681482983c3247266b35738e90e66f68e137b0ea Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 9 May 2024 12:35:29 -0400 Subject: [PATCH 14/29] Add volume units --- app/static/app/js/classes/Units.js | 39 +++++++++++++++++++ .../app/js/components/tests/Units.test.jsx | 20 ++++++++++ 2 files changed, 59 insertions(+) diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js index 4da1e3a7b..0fe54e31b 100644 --- a/app/static/app/js/classes/Units.js +++ b/app/static/app/js/classes/Units.js @@ -80,6 +80,21 @@ const units = { factor: Math.pow((3937 / 1200) / 5280, 2), abbr: 'mi² (US)', round: 5 + }, + cbmeters:{ + factor: 1, + abbr: 'm³', + round: 4 + }, + cbyards:{ + factor: Math.pow(1/(0.3048*3), 3), + abbr: 'yd³', + round: 4 + }, + cbyards_us:{ + factor: Math.pow(3937/3600, 3), + abbr: 'yd³ (US)', + round: 4 } }; @@ -105,6 +120,8 @@ class ValueUnit{ class UnitSystem{ lengthUnit(meters){ throw new Error("Not implemented"); } areaUnit(sqmeters){ throw new Error("Not implemented"); } + volumeUnit(cbmeters){ throw new Error("Not implemented"); } + getName(){ throw new Error("Not implemented"); } area(sqmeters){ @@ -118,6 +135,12 @@ class UnitSystem{ const val = unit.factor * meters; return new ValueUnit(val, unit); } + + volume(cbmeters){ + const unit = this.volumeUnit(cbmeters); + const val = unit.factor * cbmeters; + return new ValueUnit(val, unit); + } }; class MetricSystem extends UnitSystem{ @@ -136,6 +159,10 @@ class MetricSystem extends UnitSystem{ else if (sqmeters >= 1000000) return units.sqkilometers; return units.sqmeters; } + + volumeUnit(cbmeters){ + return units.cbmeters; + } } class ImperialSystem extends UnitSystem{ @@ -162,6 +189,10 @@ class ImperialSystem extends UnitSystem{ acres(){ return units.acres; } + + cbyards(){ + return units.cbyards; + } lengthUnit(meters){ const feet = this.feet().factor * meters; @@ -175,6 +206,10 @@ class ImperialSystem extends UnitSystem{ else if (sqfeet >= 27878400) return this.sqmiles(); else return this.sqfeet(); } + + volumeUnit(cbmeters){ + return this.cbyards(); + } } class ImperialUSSystem extends ImperialSystem{ @@ -201,6 +236,10 @@ class ImperialUSSystem extends ImperialSystem{ acres(){ return units.acres_us; } + + cbyards(){ + return units.cbyards_us; + } } const systems = { diff --git a/app/static/app/js/components/tests/Units.test.jsx b/app/static/app/js/components/tests/Units.test.jsx index c268ed18b..e749cc68a 100644 --- a/app/static/app/js/components/tests/Units.test.jsx +++ b/app/static/app/js/components/tests/Units.test.jsx @@ -38,6 +38,16 @@ describe('Metric system', () => { areas.forEach(a => { expect(metric.area(a[0]).toString()).toBe(a[1]); }); + + const volumes = [ + [1, "1 m³"], + [9000, "9,000 m³"], + [9000.25559, "9,000.2556 m³"], + ]; + + volumes.forEach(v => { + expect(metric.volume(v[0]).toString()).toBe(v[1]); + }); }) }); @@ -74,5 +84,15 @@ describe('Imperial systems', () => { expect(imperial.area(a[0]).toString()).toBe(a[1]); expect(imperialUS.area(a[0]).toString()).toBe(a[2]); }); + + const volumes = [ + [1, "1.308 yd³", "1.3079 yd³ (US)"], + [1000, "1,307.9506 yd³", "1,307.9428 yd³ (US)"] + ]; + + volumes.forEach(v => { + expect(imperial.volume(v[0]).toString()).toBe(v[1]); + expect(imperialUS.volume(v[0]).toString()).toBe(v[2]); + }); }) }); \ No newline at end of file From 75678b7a84e5ee5cd123aaabd1eb2041a4c4d9a2 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 9 May 2024 14:03:23 -0400 Subject: [PATCH 15/29] Fix unit selector --- app/static/app/js/components/Map.jsx | 9 ++++----- app/static/app/js/components/UnitSelector.jsx | 5 ++--- app/static/app/js/css/UnitSelector.scss | 4 ++++ 3 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 app/static/app/js/css/UnitSelector.scss diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index b04274fd8..51e397634 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -400,11 +400,6 @@ class Map extends React.Component { mapView: this }); - //add zoom control with your options - let zoomControl = Leaflet.control.zoom({ - position:'bottomleft' - }).addTo(this.map); - const UnitsCtrl = Leaflet.Control.extend({ options: { position: 'bottomleft' @@ -419,6 +414,10 @@ class Map extends React.Component { }); new UnitsCtrl().addTo(this.map); + //add zoom control with your options + let zoomControl = Leaflet.control.zoom({ + position:'bottomleft' + }).addTo(this.map); if (showBackground) { this.basemaps = {}; diff --git a/app/static/app/js/components/UnitSelector.jsx b/app/static/app/js/components/UnitSelector.jsx index 37361de17..e09ab99a8 100644 --- a/app/static/app/js/components/UnitSelector.jsx +++ b/app/static/app/js/components/UnitSelector.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { systems, getPreferredUnitSystem, setPreferredUnitSystem } from '../classes/Units'; +import '../css/UnitSelector.scss'; class UnitSelector extends React.Component { static propTypes = { @@ -12,8 +13,6 @@ class UnitSelector extends React.Component { this.state = { system: getPreferredUnitSystem() } - - // console.log(systems.metric.length(1.01).toString()); } handleChange = e => { @@ -23,7 +22,7 @@ class UnitSelector extends React.Component { render() { return ( - {Object.keys(systems).map(k => )} diff --git a/app/static/app/js/css/UnitSelector.scss b/app/static/app/js/css/UnitSelector.scss new file mode 100644 index 000000000..fd6485460 --- /dev/null +++ b/app/static/app/js/css/UnitSelector.scss @@ -0,0 +1,4 @@ +.unit-selector{ + font-size: 14px; + padding: 5px; +} \ No newline at end of file From d76eacabd3207a9641147aa481fa566db534d581 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 9 May 2024 15:46:30 -0400 Subject: [PATCH 16/29] Contours imperial preview working --- app/static/app/js/classes/Storage.js | 8 + app/static/app/js/classes/Units.js | 155 ++++++++++++++---- app/static/app/js/components/UnitSelector.jsx | 6 +- .../app/js/components/tests/Units.test.jsx | 26 ++- coreplugins/contours/manifest.json | 4 +- coreplugins/contours/public/ContoursPanel.jsx | 105 +++++++++--- 6 files changed, 242 insertions(+), 62 deletions(-) diff --git a/app/static/app/js/classes/Storage.js b/app/static/app/js/classes/Storage.js index 29f5059c8..1f5b31325 100644 --- a/app/static/app/js/classes/Storage.js +++ b/app/static/app/js/classes/Storage.js @@ -18,6 +18,14 @@ class Storage{ console.warn("Failed to call setItem " + key, e); } } + + static removeItem(key){ + try{ + localStorage.removeItem(key); + }catch(e){ + console.warn("Failed to call removeItem " + key, e); + } + } } export default Storage; \ No newline at end of file diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js index 0fe54e31b..c647ad0ae 100644 --- a/app/static/app/js/classes/Units.js +++ b/app/static/app/js/classes/Units.js @@ -1,112 +1,156 @@ import { _ } from './gettext'; +const types = { + LENGTH: 1, + AREA: 2, + VOLUME: 3 +}; + const units = { acres: { factor: (1 / (0.3048 * 0.3048)) / 43560, abbr: 'ac', - round: 5 + round: 5, + label: _("Acres"), + type: types.AREA }, acres_us: { factor: Math.pow(3937 / 1200, 2) / 43560, abbr: 'ac (US)', - round: 5 + round: 5, + label: _("Acres"), + type: types.AREA }, feet: { factor: 1 / 0.3048, abbr: 'ft', - round: 4 + round: 4, + label: _("Feet"), + type: types.LENGTH }, feet_us:{ factor: 3937 / 1200, abbr: 'ft (US)', - round: 4 + round: 4, + label: _("Feet"), + type: types.LENGTH }, hectares: { factor: 0.0001, abbr: 'ha', - round: 4 + round: 4, + label: _("Hectares"), + type: types.AREA }, meters: { factor: 1, abbr: 'm', - round: 3 + round: 3, + label: _("Meters"), + type: types.LENGTH }, kilometers: { factor: 0.001, abbr: 'km', - round: 5 + round: 5, + label: _("Kilometers"), + type: types.LENGTH }, centimeters: { factor: 100, abbr: 'cm', - round: 1 + round: 1, + label: _("Centimeters"), + type: types.LENGTH }, miles: { factor: (1 / 0.3048) / 5280, abbr: 'mi', - round: 5 - }, + round: 5, + label: _("Miles"), + type: types.LENGTH + }, miles_us: { factor: (3937 / 1200) / 5280, abbr: 'mi (US)', - round: 5 + round: 5, + label: _("Miles"), + type: types.LENGTH }, sqfeet: { factor: 1 / (0.3048 * 0.3048), abbr: 'ft²', - round: 2 + round: 2, + label: _("Squared Feet"), + type: types.AREA }, sqfeet_us: { factor: Math.pow(3937 / 1200, 2), abbr: 'ft² (US)', - round: 2 + round: 2, + label: _("Squared Feet"), + type: types.AREA }, sqmeters: { factor: 1, abbr: 'm²', - round: 2 + round: 2, + label: _("Squared Meters"), + type: types.AREA }, sqkilometers: { factor: 0.000001, abbr: 'km²', - round: 5 + round: 5, + label: _("Squared Kilometers"), + type: types.AREA }, sqmiles: { factor: Math.pow((1 / 0.3048) / 5280, 2), abbr: 'mi²', - round: 5 + round: 5, + label: _("Squared Miles"), + type: types.AREA }, sqmiles_us: { factor: Math.pow((3937 / 1200) / 5280, 2), abbr: 'mi² (US)', - round: 5 + round: 5, + label: _("Squared Miles"), + type: types.AREA }, cbmeters:{ factor: 1, abbr: 'm³', - round: 4 + round: 4, + label: _("Cubic Meters"), + type: types.VOLUME }, cbyards:{ factor: Math.pow(1/(0.3048*3), 3), abbr: 'yd³', - round: 4 + round: 4, + label: _("Cubic Yards"), + type: types.VOLUME }, cbyards_us:{ factor: Math.pow(3937/3600, 3), abbr: 'yd³ (US)', - round: 4 + round: 4, + label: _("Cubic Yards"), + type: types.VOLUME } }; class ValueUnit{ - constructor(val, unit){ - this.val = val; + constructor(value, unit){ + this.value = value; this.unit = unit; } toString(){ const mul = Math.pow(10, this.unit.round); - const rounded = (Math.round(this.val * mul) / mul).toString(); + const rounded = (Math.round(this.value * mul) / mul).toString(); let withCommas = ""; let parts = rounded.split("."); @@ -117,6 +161,12 @@ class ValueUnit{ } } +class NanUnit{ + toString(){ + return "NaN"; + } +} + class UnitSystem{ lengthUnit(meters){ throw new Error("Not implemented"); } areaUnit(sqmeters){ throw new Error("Not implemented"); } @@ -125,24 +175,55 @@ class UnitSystem{ getName(){ throw new Error("Not implemented"); } area(sqmeters){ + sqmeters = parseFloat(sqmeters); + if (isNaN(sqmeters)) return NanUnit(); + const unit = this.areaUnit(sqmeters); const val = unit.factor * sqmeters; return new ValueUnit(val, unit); } length(meters){ + meters = parseFloat(meters); + if (isNaN(meters)) return NanUnit(); + const unit = this.lengthUnit(meters); const val = unit.factor * meters; return new ValueUnit(val, unit); } volume(cbmeters){ + cbmeters = parseFloat(cbmeters); + if (isNaN(cbmeters)) return NanUnit(); + const unit = this.volumeUnit(cbmeters); const val = unit.factor * cbmeters; return new ValueUnit(val, unit); } }; +function toMetric(valueUnit, unit){ + let value = NaN; + if (typeof valueUnit === "object" && unit === undefined){ + value = valueUnit.value; + unit = valueUnit.unit; + }else{ + value = parseFloat(valueUnit); + } + if (isNaN(value)) return NanUnit(); + + const val = value / unit.factor; + if (unit.type === types.LENGTH){ + return new ValueUnit(val, units.meters); + }else if (unit.type === types.AREA){ + return new ValueUnit(val, unit.sqmeters); + }else if (unit.type === types.VOLUME){ + return new ValueUnit(val, unit.cbmeters); + }else{ + throw new Error(`Unrecognized unit type: ${unit.type}`); + } +} + class MetricSystem extends UnitSystem{ getName(){ return _("Metric"); @@ -249,16 +330,32 @@ const systems = { } // Expose to allow every part of the app to access this information -function getPreferredUnitSystem(){ - return localStorage.getItem("preferred_unit_system") || "metric"; +function getUnitSystem(){ + return localStorage.getItem("_unit_system") || "metric"; } -function setPreferredUnitSystem(system){ - localStorage.setItem("preferred_unit_system", system); +function setUnitSystem(system){ + let prevSystem = getUnitSystem(); + localStorage.setItem("_unit_system", system); + if (prevSystem !== system){ + document.dispatchEvent(new CustomEvent("onUnitSystemChanged", { detail: system })); + } +} + +function onUnitSystemChanged(callback){ + document.addEventListener("onUnitSystemChanged", callback); +} + +function offUnitSystemChanged(callback){ + document.removeEventListener("onUnitSystemChanged", callback); } export { systems, - getPreferredUnitSystem, - setPreferredUnitSystem + types, + toMetric, + getUnitSystem, + setUnitSystem, + onUnitSystemChanged, + offUnitSystemChanged }; diff --git a/app/static/app/js/components/UnitSelector.jsx b/app/static/app/js/components/UnitSelector.jsx index e09ab99a8..5b5e678ed 100644 --- a/app/static/app/js/components/UnitSelector.jsx +++ b/app/static/app/js/components/UnitSelector.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { systems, getPreferredUnitSystem, setPreferredUnitSystem } from '../classes/Units'; +import { systems, getUnitSystem, setUnitSystem } from '../classes/Units'; import '../css/UnitSelector.scss'; class UnitSelector extends React.Component { @@ -11,13 +11,13 @@ class UnitSelector extends React.Component { super(props); this.state = { - system: getPreferredUnitSystem() + system: getUnitSystem() } } handleChange = e => { this.setState({system: e.target.value}); - setPreferredUnitSystem(e.target.value); + setUnitSystem(e.target.value); }; render() { diff --git a/app/static/app/js/components/tests/Units.test.jsx b/app/static/app/js/components/tests/Units.test.jsx index e749cc68a..72bea77d0 100644 --- a/app/static/app/js/components/tests/Units.test.jsx +++ b/app/static/app/js/components/tests/Units.test.jsx @@ -1,4 +1,4 @@ -import { systems } from '../../classes/Units'; +import { systems, toMetric } from '../../classes/Units'; describe('Metric system', () => { it('it should display units properly', () => { @@ -94,5 +94,25 @@ describe('Imperial systems', () => { expect(imperial.volume(v[0]).toString()).toBe(v[1]); expect(imperialUS.volume(v[0]).toString()).toBe(v[2]); }); - }) -}); \ No newline at end of file + }); +}); + +describe('Metric conversion', () => { + it('it should convert units properly', () => { + const { metric, imperial } = systems; + + const km = metric.length(2000); + const mi = imperial.length(3220); + + expect(km.unit.abbr).toBe("km"); + expect(km.value).toBe(2); + expect(mi.unit.abbr).toBe("mi"); + expect(Math.round(mi.value)).toBe(2) + + expect(toMetric(km).toString()).toBe("2,000 m"); + expect(toMetric(mi).toString()).toBe("3,220 m"); + + expect(toMetric(km).value).toBe(2000); + expect(toMetric(mi).value).toBe(3220); + }); +}); diff --git a/coreplugins/contours/manifest.json b/coreplugins/contours/manifest.json index a47d2c59c..62c4b5d0e 100644 --- a/coreplugins/contours/manifest.json +++ b/coreplugins/contours/manifest.json @@ -1,8 +1,8 @@ { "name": "Contours", - "webodmMinVersion": "0.9.0", + "webodmMinVersion": "2.4.3", "description": "Compute, preview and export contours from DEMs", - "version": "1.0.0", + "version": "1.1.0", "author": "Piero Toffanin", "email": "pt@masseranolabs.com", "repository": "https://github.com/OpenDroneMap/WebODM", diff --git a/coreplugins/contours/public/ContoursPanel.jsx b/coreplugins/contours/public/ContoursPanel.jsx index f1502b9a1..b60cdfb09 100644 --- a/coreplugins/contours/public/ContoursPanel.jsx +++ b/coreplugins/contours/public/ContoursPanel.jsx @@ -6,6 +6,7 @@ import './ContoursPanel.scss'; import ErrorMessage from 'webodm/components/ErrorMessage'; import Workers from 'webodm/classes/Workers'; import { _ } from 'webodm/classes/gettext'; +import { systems, getUnitSystem, onUnitSystemChanged, offUnitSystemChanged, toMetric } from 'webodm/classes/Units'; export default class ContoursPanel extends React.Component { static defaultProps = { @@ -20,13 +21,23 @@ export default class ContoursPanel extends React.Component { constructor(props){ super(props); + const unitSystem = getUnitSystem(); + const defaultInterval = unitSystem === "metric" ? "1" : "4"; + const defaultSimplify = unitSystem === "metric" ? "0.2" : "0.6"; + + // Remove legacy parameters + Storage.removeItem("last_contours_interval"); + Storage.removeItem("last_contours_custom_interval"); + Storage.removeItem("last_contours_simplify"); + Storage.removeItem("last_contours_custom_simplify"); + this.state = { error: "", permanentError: "", - interval: Storage.getItem("last_contours_interval") || "1", - customInterval: Storage.getItem("last_contours_custom_interval") || "1", - simplify: Storage.getItem("last_contours_simplify") || "0.2", - customSimplify: Storage.getItem("last_contours_custom_simplify") || "0.2", + interval: Storage.getItem("last_contours_interval_" + unitSystem) || defaultInterval, + customInterval: Storage.getItem("last_contours_custom_interval_" + unitSystem) || defaultInterval, + simplify: Storage.getItem("last_contours_simplify_" + unitSystem) || defaultSimplify, + customSimplify: Storage.getItem("last_contours_custom_simplify_" + unitSystem) || defaultSimplify, layer: "", epsg: Storage.getItem("last_contours_epsg") || "4326", customEpsg: Storage.getItem("last_contours_custom_epsg") || "4326", @@ -36,13 +47,18 @@ export default class ContoursPanel extends React.Component { previewLoading: false, exportLoading: false, previewLayer: null, + unitSystem }; } + componentDidMount(){ + onUnitSystemChanged(this.unitsChanged); + } + componentDidUpdate(){ if (this.props.isShowed && this.state.loading){ const {id, project} = this.state.task; - + this.loadingReq = $.getJSON(`/api/projects/${project}/tasks/${id}/`) .done(res => { const { available_assets } = res; @@ -76,6 +92,24 @@ export default class ContoursPanel extends React.Component { this.generateReq.abort(); this.generateReq = null; } + + offUnitSystemChanged(this.unitsChanged); + } + + unitsChanged = e => { + this.saveInputValues(); + + const unitSystem = e.detail; + + const defaultInterval = unitSystem === "metric" ? "1" : "4"; + const defaultSimplify = unitSystem === "metric" ? "0.2" : "0.5"; + + const interval = Storage.getItem("last_contours_interval_" + unitSystem) || defaultInterval; + const customInterval = Storage.getItem("last_contours_custom_interval_" + unitSystem) || defaultInterval; + const simplify = Storage.getItem("last_contours_simplify_" + unitSystem) || defaultSimplify; + const customSimplify = Storage.getItem("last_contours_custom_simplify_" + unitSystem) || defaultSimplify; + + this.setState({unitSystem, interval, customInterval, simplify, customSimplify }); } handleSelectInterval = e => { @@ -108,17 +142,29 @@ export default class ContoursPanel extends React.Component { getFormValues = () => { const { interval, customInterval, epsg, customEpsg, - simplify, customSimplify, layer } = this.state; + simplify, customSimplify, layer, unitSystem } = this.state; + const su = systems[unitSystem]; + + let meterInterval = interval !== "custom" ? interval : customInterval; + let meterSimplify = simplify !== "custom" ? simplify : customSimplify; + + meterInterval = toMetric(meterInterval, su.lengthUnit(1)).value; + meterSimplify = toMetric(meterSimplify, su.lengthUnit(1)).value; + + const zExportFactor = su.lengthUnit(1).factor; + return { - interval: interval !== "custom" ? interval : customInterval, + interval: meterInterval, epsg: epsg !== "custom" ? epsg : customEpsg, - simplify: simplify !== "custom" ? simplify : customSimplify, + simplify: meterSimplify, + zExportFactor, layer }; } addGeoJSONFromURL = (url, cb) => { const { map } = this.props; + const us = systems[this.state.unitSystem]; $.getJSON(url) .done((geojson) => { @@ -128,7 +174,7 @@ export default class ContoursPanel extends React.Component { this.setState({previewLayer: L.geoJSON(geojson, { onEachFeature: (feature, layer) => { if (feature.properties && feature.properties.level !== undefined) { - layer.bindPopup(`${_("Elevation:")} ${feature.properties.level} ${_("meters")}`); + layer.bindPopup(`
${_("Elevation:")} ${us.length(feature.properties.level)}
`); } }, style: feature => { @@ -155,18 +201,23 @@ export default class ContoursPanel extends React.Component { } } + saveInputValues = () => { + const us = this.state.unitSystem; + + // Save settings + Storage.setItem("last_contours_interval_" + us, this.state.interval); + Storage.setItem("last_contours_custom_interval_" + us, this.state.customInterval); + Storage.setItem("last_contours_simplify_" + us, this.state.simplify); + Storage.setItem("last_contours_custom_simplify_" + us, this.state.customSimplify); + Storage.setItem("last_contours_epsg", this.state.epsg); + Storage.setItem("last_contours_custom_epsg", this.state.customEpsg); + } + generateContours = (data, loadingProp, isPreview) => { this.setState({[loadingProp]: true, error: ""}); const taskId = this.state.task.id; + this.saveInputValues(); - // Save settings for next time - Storage.setItem("last_contours_interval", this.state.interval); - Storage.setItem("last_contours_custom_interval", this.state.customInterval); - Storage.setItem("last_contours_simplify", this.state.simplify); - Storage.setItem("last_contours_custom_simplify", this.state.customSimplify); - Storage.setItem("last_contours_epsg", this.state.epsg); - Storage.setItem("last_contours_custom_epsg", this.state.customEpsg); - this.generateReq = $.ajax({ type: 'POST', url: `/api/plugins/contours/task/${taskId}/contours/generate`, @@ -222,11 +273,15 @@ export default class ContoursPanel extends React.Component { const { loading, task, layers, error, permanentError, interval, customInterval, layer, epsg, customEpsg, exportLoading, simplify, customSimplify, - previewLoading, previewLayer } = this.state; - const intervalValues = [0.25, 0.5, 1, 1.5, 2]; + previewLoading, previewLayer, unitSystem } = this.state; + const us = systems[unitSystem]; + const lengthUnit = us.lengthUnit(1); + + const intervalStart = unitSystem === "metric" ? 1 : 4; + const intervalValues = [intervalStart / 4, intervalStart / 2, intervalStart, intervalStart * 2, intervalStart * 4]; const simplifyValues = [{label: _('Do not simplify'), value: 0}, - {label: _('Normal'), value: 0.2}, - {label: _('Aggressive'), value: 1}]; + {label: _('Normal'), value: unitSystem === "metric" ? 0.2 : 0.5}, + {label: _('Aggressive'), value: unitSystem === "metric" ? 1 : 4}]; const disabled = (interval === "custom" && !customInterval) || (epsg === "custom" && !customEpsg) || @@ -242,7 +297,7 @@ export default class ContoursPanel extends React.Component {
@@ -251,7 +306,7 @@ export default class ContoursPanel extends React.Component {
- {_("meter")} + {lengthUnit.label}
: ""} @@ -269,7 +324,7 @@ export default class ContoursPanel extends React.Component {
@@ -278,7 +333,7 @@ export default class ContoursPanel extends React.Component {
- {_("meter")} + {lengthUnit.label}
: ""} From 1fc7e11c865dc04e88de4b7a313db20beadcbb13 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 10 May 2024 13:44:12 -0400 Subject: [PATCH 17/29] Contours plugin imperial units export/preview working --- coreplugins/contours/api.py | 7 ++++--- coreplugins/contours/public/ContoursPanel.jsx | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/coreplugins/contours/api.py b/coreplugins/contours/api.py index 603bc2175..026eab613 100644 --- a/coreplugins/contours/api.py +++ b/coreplugins/contours/api.py @@ -9,7 +9,7 @@ class ContoursException(Exception): pass -def calc_contours(dem, epsg, interval, output_format, simplify): +def calc_contours(dem, epsg, interval, output_format, simplify, zfactor = 1): import os import subprocess import tempfile @@ -50,7 +50,7 @@ def calc_contours(dem, epsg, interval, output_format, simplify): outfile = os.path.join(tmpdir, f"output.{ext}") p = subprocess.Popen([ogr2ogr_bin, outfile, contours_file, "-simplify", str(simplify), "-f", output_format, "-t_srs", f"EPSG:{epsg}", "-nln", "contours", - "-dialect", "sqlite", "-sql", f"SELECT * FROM contour WHERE ST_Length(GEOM) >= {MIN_CONTOUR_LENGTH}"], cwd=tmpdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + "-dialect", "sqlite", "-sql", f"SELECT ID, ROUND(level * {zfactor}, 5) AS level, GeomFromGML(AsGML(ATM_Transform(GEOM, ATM_Scale(ATM_Create(), 1, 1, {zfactor})), 10)) as GEOM FROM contour WHERE ST_Length(GEOM) >= {MIN_CONTOUR_LENGTH}"], cwd=tmpdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() out = out.decode('utf-8').strip() @@ -102,8 +102,9 @@ def post(self, request, pk=None): if not format in supported_formats: raise ContoursException("Invalid format {} (must be one of: {})".format(format, ",".join(supported_formats))) simplify = float(request.data.get('simplify', 0.01)) + zfactor = float(request.data.get('zfactor', 1)) - celery_task_id = run_function_async(calc_contours, dem, epsg, interval, format, simplify).task_id + celery_task_id = run_function_async(calc_contours, dem, epsg, interval, format, simplify, zfactor).task_id return Response({'celery_task_id': celery_task_id}, status=status.HTTP_200_OK) except ContoursException as e: return Response({'error': str(e)}, status=status.HTTP_200_OK) diff --git a/coreplugins/contours/public/ContoursPanel.jsx b/coreplugins/contours/public/ContoursPanel.jsx index b60cdfb09..e77f13099 100644 --- a/coreplugins/contours/public/ContoursPanel.jsx +++ b/coreplugins/contours/public/ContoursPanel.jsx @@ -140,7 +140,7 @@ export default class ContoursPanel extends React.Component { this.setState({customEpsg: e.target.value}); } - getFormValues = () => { + getFormValues = (preview) => { const { interval, customInterval, epsg, customEpsg, simplify, customSimplify, layer, unitSystem } = this.state; const su = systems[unitSystem]; @@ -151,13 +151,13 @@ export default class ContoursPanel extends React.Component { meterInterval = toMetric(meterInterval, su.lengthUnit(1)).value; meterSimplify = toMetric(meterSimplify, su.lengthUnit(1)).value; - const zExportFactor = su.lengthUnit(1).factor; + const zfactor = preview ? 1 : su.lengthUnit(1).factor; return { interval: meterInterval, epsg: epsg !== "custom" ? epsg : customEpsg, simplify: meterSimplify, - zExportFactor, + zfactor, layer }; } @@ -254,7 +254,7 @@ export default class ContoursPanel extends React.Component { handleExport = (format) => { return () => { - const data = this.getFormValues(); + const data = this.getFormValues(false); data.format = format; this.generateContours(data, 'exportLoading', false); }; @@ -263,7 +263,7 @@ export default class ContoursPanel extends React.Component { handleShowPreview = () => { this.setState({previewLoading: true}); - const data = this.getFormValues(); + const data = this.getFormValues(true); data.epsg = 4326; data.format = "GeoJSON"; this.generateContours(data, 'previewLoading', true); From 3b55ebd3e5e9f8b483addf8be288bc3ecd18716d Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 10 May 2024 21:24:34 -0400 Subject: [PATCH 18/29] Add unit options --- app/static/app/js/classes/Units.js | 51 ++++++++++++------- app/static/app/js/components/Map.jsx | 8 +++ .../app/js/components/tests/Units.test.jsx | 4 ++ 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js index c647ad0ae..6a3b665e1 100644 --- a/app/static/app/js/classes/Units.js +++ b/app/static/app/js/classes/Units.js @@ -148,8 +148,8 @@ class ValueUnit{ this.unit = unit; } - toString(){ - const mul = Math.pow(10, this.unit.round); + toString(opts = {}){ + const mul = Math.pow(10, opts.precision !== undefined ? opts.precision : this.unit.round); const rounded = (Math.round(this.value * mul) / mul).toString(); let withCommas = ""; @@ -168,35 +168,35 @@ class NanUnit{ } class UnitSystem{ - lengthUnit(meters){ throw new Error("Not implemented"); } - areaUnit(sqmeters){ throw new Error("Not implemented"); } - volumeUnit(cbmeters){ throw new Error("Not implemented"); } + lengthUnit(meters, opts = {}){ throw new Error("Not implemented"); } + areaUnit(sqmeters, opts = {}){ throw new Error("Not implemented"); } + volumeUnit(cbmeters, opts = {}){ throw new Error("Not implemented"); } getName(){ throw new Error("Not implemented"); } - area(sqmeters){ + area(sqmeters, opts = {}){ sqmeters = parseFloat(sqmeters); if (isNaN(sqmeters)) return NanUnit(); - const unit = this.areaUnit(sqmeters); + const unit = this.areaUnit(sqmeters, opts); const val = unit.factor * sqmeters; return new ValueUnit(val, unit); } - length(meters){ + length(meters, opts = {}){ meters = parseFloat(meters); if (isNaN(meters)) return NanUnit(); - const unit = this.lengthUnit(meters); + const unit = this.lengthUnit(meters, opts); const val = unit.factor * meters; return new ValueUnit(val, unit); } - volume(cbmeters){ + volume(cbmeters, opts = {}){ cbmeters = parseFloat(cbmeters); if (isNaN(cbmeters)) return NanUnit(); - const unit = this.volumeUnit(cbmeters); + const unit = this.volumeUnit(cbmeters, opts); const val = unit.factor * cbmeters; return new ValueUnit(val, unit); } @@ -229,19 +229,23 @@ class MetricSystem extends UnitSystem{ return _("Metric"); } - lengthUnit(meters){ + lengthUnit(meters, opts = {}){ + if (opts.fixedUnit) return units.meters; + if (meters < 1) return units.centimeters; else if (meters >= 1000) return units.kilometers; else return units.meters; } - areaUnit(sqmeters){ + areaUnit(sqmeters, opts = {}){ + if (opts.fixedUnit) return units.sqmeters; + if (sqmeters >= 10000 && sqmeters < 1000000) return units.hectares; else if (sqmeters >= 1000000) return units.sqkilometers; return units.sqmeters; } - volumeUnit(cbmeters){ + volumeUnit(cbmeters, opts = {}){ return units.cbmeters; } } @@ -275,20 +279,24 @@ class ImperialSystem extends UnitSystem{ return units.cbyards; } - lengthUnit(meters){ + lengthUnit(meters, opts = {}){ + if (opts.fixedUnit) return this.feet(); + const feet = this.feet().factor * meters; if (feet >= 5280) return this.miles(); else return this.feet(); } - areaUnit(sqmeters){ + areaUnit(sqmeters, opts = {}){ + if (opts.fixedUnit) return this.sqfeet(); + const sqfeet = this.sqfeet().factor * sqmeters; if (sqfeet >= 43560 && sqfeet < 27878400) return this.acres(); else if (sqfeet >= 27878400) return this.sqmiles(); else return this.sqfeet(); } - volumeUnit(cbmeters){ + volumeUnit(cbmeters, opts = {}){ return this.cbyards(); } } @@ -331,11 +339,11 @@ const systems = { // Expose to allow every part of the app to access this information function getUnitSystem(){ - return localStorage.getItem("_unit_system") || "metric"; + return localStorage.getItem("unit_system") || "metric"; } function setUnitSystem(system){ let prevSystem = getUnitSystem(); - localStorage.setItem("_unit_system", system); + localStorage.setItem("unit_system", system); if (prevSystem !== system){ document.dispatchEvent(new CustomEvent("onUnitSystemChanged", { detail: system })); } @@ -349,10 +357,15 @@ function offUnitSystemChanged(callback){ document.removeEventListener("onUnitSystemChanged", callback); } +function unitSystem(){ + return systems[getUnitSystem()]; +} + export { systems, types, toMetric, + unitSystem, getUnitSystem, setUnitSystem, onUnitSystemChanged, diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index 51e397634..e207948d7 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -28,6 +28,7 @@ import 'rbush'; import '../vendor/leaflet/leaflet-markers-canvas'; import { _ } from '../classes/gettext'; import UnitSelector from './UnitSelector'; +import { unitSystem } from '../classes/Units'; class Map extends React.Component { static defaultProps = { @@ -134,6 +135,9 @@ class Map extends React.Component { const { url, meta, type } = tile; let metaUrl = url + "metadata"; + let histUnit = (value, precision) => { + return value.toFixed(precision); + }; if (type == "plant"){ if (meta.task && meta.task.orthophoto_bands && meta.task.orthophoto_bands.length === 2){ @@ -149,6 +153,9 @@ class Map extends React.Component { } }else if (type == "dsm" || type == "dtm"){ metaUrl += "?hillshade=6&color_map=viridis"; + histUnit = (value, precision) => { + return unitSystem().length(value, { fixedUnit: true }).toString({precision}); + }; } this.tileJsonRequests.push($.getJSON(metaUrl) @@ -209,6 +216,7 @@ class Map extends React.Component { // Associate metadata with this layer meta.name = name + ` (${this.typeToHuman(type)})`; meta.metaUrl = metaUrl; + meta.histUnit = histUnit; layer[Symbol.for("meta")] = meta; layer[Symbol.for("tile-meta")] = mres; diff --git a/app/static/app/js/components/tests/Units.test.jsx b/app/static/app/js/components/tests/Units.test.jsx index 72bea77d0..174b38aaa 100644 --- a/app/static/app/js/components/tests/Units.test.jsx +++ b/app/static/app/js/components/tests/Units.test.jsx @@ -48,6 +48,8 @@ describe('Metric system', () => { volumes.forEach(v => { expect(metric.volume(v[0]).toString()).toBe(v[1]); }); + + expect(metric.area(11005.09, { fixedUnit: true }).toString({precision: 1})).toBe("11,005.1 m²"); }) }); @@ -94,6 +96,8 @@ describe('Imperial systems', () => { expect(imperial.volume(v[0]).toString()).toBe(v[1]); expect(imperialUS.volume(v[0]).toString()).toBe(v[2]); }); + + expect(imperial.area(9999, { fixedUnit: true }).toString({precision: 1})).toBe("107,628.3 ft²"); }); }); From 4e2ffbb7680d28b7d3e9e549392eceab86e41cf0 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 10 May 2024 22:01:37 -0400 Subject: [PATCH 19/29] PoC elevation histogram imperial units display --- app/static/app/js/components/Histogram.jsx | 20 +++++++++++++++---- .../app/js/components/LayersControlLayer.jsx | 2 ++ app/static/app/js/components/Map.jsx | 17 +++++++++------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app/static/app/js/components/Histogram.jsx b/app/static/app/js/components/Histogram.jsx index 2be1d7b4b..3d1598971 100644 --- a/app/static/app/js/components/Histogram.jsx +++ b/app/static/app/js/components/Histogram.jsx @@ -8,6 +8,8 @@ export default class Histogram extends React.Component { static defaultProps = { width: 280, colorMap: null, + unitForward: value => value, + unitBackward: value => value, onUpdate: null, loading: false, min: null, @@ -16,6 +18,8 @@ export default class Histogram extends React.Component { static propTypes = { statistics: PropTypes.object.isRequired, colorMap: PropTypes.array, + unitForward: PropTypes.func, + unitBackward: PropTypes.func, width: PropTypes.number, onUpdate: PropTypes.func, loading: PropTypes.bool, @@ -68,8 +72,8 @@ export default class Histogram extends React.Component { const st = { min: min, max: max, - minInput: min.toFixed(3), - maxInput: max.toFixed(3) + minInput: this.props.unitForward(min).toFixed(3), + maxInput: this.props.unitForward(max).toFixed(3) }; if (!this.state){ @@ -113,11 +117,14 @@ export default class Histogram extends React.Component { let x = d3.scale.linear() .domain(this.rangeX) .range([0, width]); + let tickFormat = x => { + return this.props.unitForward(x).toFixed(0); + }; svg.append("g") .attr("class", "x axis theme-fill-primary") .attr("transform", "translate(0," + (height - 5) + ")") - .call(d3.svg.axis().scale(x).tickValues(this.rangeX).orient("bottom")); + .call(d3.svg.axis().scale(x).tickValues(this.rangeX).tickFormat(tickFormat).orient("bottom")); // add the y Axis let y = d3.scale.linear() @@ -250,7 +257,10 @@ export default class Histogram extends React.Component { componentDidUpdate(prevProps, prevState){ if (prevState.min !== this.state.min || prevState.max !== this.state.max){ - this.setState({minInput: this.state.min.toFixed(3), maxInput: this.state.max.toFixed(3)}); + this.setState({ + minInput: this.props.unitForward(this.state.min).toFixed(3), + maxInput: this.props.unitForward(this.state.max).toFixed(3) + }); } if (prevState.min !== this.state.min || @@ -295,6 +305,7 @@ export default class Histogram extends React.Component { handleMaxBlur = (e) => { let val = parseFloat(e.target.value); if (!isNaN(val)){ + val = this.props.unitBackward(val); val = Math.max(this.state.min, Math.min(this.rangeX[1], val)); this.setState({max: val, maxInput: val.toFixed(3)}); } @@ -311,6 +322,7 @@ export default class Histogram extends React.Component { handleMinBlur = (e) => { let val = parseFloat(e.target.value); if (!isNaN(val)){ + val = this.props.unitBackward(val); val = Math.max(this.rangeX[0], Math.min(this.state.max, val)); this.setState({min: val, minInput: val.toFixed(3)}); } diff --git a/app/static/app/js/components/LayersControlLayer.jsx b/app/static/app/js/components/LayersControlLayer.jsx index 8de09ac60..15c710323 100644 --- a/app/static/app/js/components/LayersControlLayer.jsx +++ b/app/static/app/js/components/LayersControlLayer.jsx @@ -295,6 +295,8 @@ export default class LayersControlLayer extends React.Component { { - return value.toFixed(precision); - }; + let unitForward = value => value; + let unitBackward = value => value; if (type == "plant"){ if (meta.task && meta.task.orthophoto_bands && meta.task.orthophoto_bands.length === 2){ @@ -153,8 +152,11 @@ class Map extends React.Component { } }else if (type == "dsm" || type == "dtm"){ metaUrl += "?hillshade=6&color_map=viridis"; - histUnit = (value, precision) => { - return unitSystem().length(value, { fixedUnit: true }).toString({precision}); + unitForward = value => { + return unitSystem().length(value, { fixedUnit: true }).value; + }; + unitBackward = value => { + return toMetric(value).value; }; } @@ -216,7 +218,8 @@ class Map extends React.Component { // Associate metadata with this layer meta.name = name + ` (${this.typeToHuman(type)})`; meta.metaUrl = metaUrl; - meta.histUnit = histUnit; + meta.unitForward = unitForward; + meta.unitBackward = unitBackward; layer[Symbol.for("meta")] = meta; layer[Symbol.for("tile-meta")] = mres; From 57ccd2323488e2731861c984c074366a1ccfc815 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Sat, 11 May 2024 12:43:40 -0400 Subject: [PATCH 20/29] Fix login next redirect --- app/static/app/js/components/Histogram.jsx | 11 +++++++++++ app/templates/app/registration/login.html | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/app/static/app/js/components/Histogram.jsx b/app/static/app/js/components/Histogram.jsx index 3d1598971..311dc96dd 100644 --- a/app/static/app/js/components/Histogram.jsx +++ b/app/static/app/js/components/Histogram.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import '../css/Histogram.scss'; import d3 from 'd3'; import { _ } from '../classes/gettext'; +import { onUnitSystemChanged, offUnitSystemChanged } from '../classes/Units'; export default class Histogram extends React.Component { static defaultProps = { @@ -253,6 +254,16 @@ export default class Histogram extends React.Component { componentDidMount(){ this.redraw(); + onUnitSystemChanged(this.handleUnitSystemChanged); + } + + componentWillUnmount(){ + offUnitSystemChanged(this.handleUnitSystemChanged); + } + + handleUnitSystemChanged = e => { + this.redraw(); + this.forceUpdate(); } componentDidUpdate(prevProps, prevState){ diff --git a/app/templates/app/registration/login.html b/app/templates/app/registration/login.html index c19260f2a..8cd90d883 100644 --- a/app/templates/app/registration/login.html +++ b/app/templates/app/registration/login.html @@ -20,6 +20,12 @@ {% for field in form %} {% include 'registration/form_field.html' %} {% endfor %} + +
From 35fc60aa2c44d3054200ed9b2153a01b83e14d8e Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Sat, 11 May 2024 12:51:37 -0400 Subject: [PATCH 21/29] Elevation layer units update works --- app/static/app/js/components/Histogram.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/static/app/js/components/Histogram.jsx b/app/static/app/js/components/Histogram.jsx index 311dc96dd..2d0c2a8ed 100644 --- a/app/static/app/js/components/Histogram.jsx +++ b/app/static/app/js/components/Histogram.jsx @@ -263,7 +263,10 @@ export default class Histogram extends React.Component { handleUnitSystemChanged = e => { this.redraw(); - this.forceUpdate(); + this.setState({ + minInput: this.props.unitForward(this.state.min).toFixed(3), + maxInput: this.props.unitForward(this.state.max).toFixed(3) + }); } componentDidUpdate(prevProps, prevState){ From 8468fdff5c819149a1fcd95a6b3d9cc429fc3caa Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Sat, 11 May 2024 15:01:26 -0400 Subject: [PATCH 22/29] Refactor get_asset_file_or_stream --- app/api/tasks.py | 11 ++++++----- app/models/task.py | 8 ++++---- coreplugins/dronedb/api_views.py | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/api/tasks.py b/app/api/tasks.py index 2f0167cbe..ed02314ed 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -365,19 +365,20 @@ def get(self, request, pk=None, project_pk=None, asset=""): # Check and download try: - asset_fs, is_zipstream = task.get_asset_file_or_zipstream(asset) + asset_fs = task.get_asset_file_or_stream(asset) except FileNotFoundError: raise exceptions.NotFound(_("Asset does not exist")) - if not is_zipstream and not os.path.isfile(asset_fs): + is_stream = not isinstance(asset_fs, str) + if not is_stream and not os.path.isfile(asset_fs): raise exceptions.NotFound(_("Asset does not exist")) download_filename = request.GET.get('filename', get_asset_download_filename(task, asset)) - if not is_zipstream: - return download_file_response(request, asset_fs, 'attachment', download_filename=download_filename) - else: + if is_stream: return download_file_stream(request, asset_fs, 'attachment', download_filename=download_filename) + else: + return download_file_response(request, asset_fs, 'attachment', download_filename=download_filename) """ Raw access to the task's asset folder resources diff --git a/app/models/task.py b/app/models/task.py index 9c007138f..7b369893d 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -450,16 +450,16 @@ def duplicate(self, set_new_name=True): return False - def get_asset_file_or_zipstream(self, asset): + def get_asset_file_or_stream(self, asset): """ Get a stream to an asset :param asset: one of ASSETS_MAP keys - :return: (path|stream, is_zipstream:bool) + :return: (path|stream) """ if asset in self.ASSETS_MAP: value = self.ASSETS_MAP[asset] if isinstance(value, str): - return self.assets_path(value), False + return self.assets_path(value) elif isinstance(value, dict): if 'deferred_path' in value and 'deferred_compress_dir' in value: @@ -469,7 +469,7 @@ def get_asset_file_or_zipstream(self, asset): paths = [p for p in paths if os.path.basename(p['fs']) not in value['deferred_exclude_files']] if len(paths) == 0: raise FileNotFoundError("No files available for download") - return zipfly.ZipStream(paths), True + return zipfly.ZipStream(paths) else: raise FileNotFoundError("{} is not a valid asset (invalid dict values)".format(asset)) else: diff --git a/coreplugins/dronedb/api_views.py b/coreplugins/dronedb/api_views.py index 673556ad3..733789029 100644 --- a/coreplugins/dronedb/api_views.py +++ b/coreplugins/dronedb/api_views.py @@ -343,7 +343,7 @@ def post(self, request, pk): settings = get_settings(request) - available_assets = [task.get_asset_file_or_zipstream(f)[0] for f in list(set(task.available_assets) & set(DRONEDB_ASSETS))] + available_assets = [task.get_asset_file_or_stream(f) for f in list(set(task.available_assets) & set(DRONEDB_ASSETS))] if 'textured_model.zip' in task.available_assets: texture_files = [join(task.assets_path('odm_texturing'), f) for f in listdir(task.assets_path('odm_texturing')) if isfile(join(task.assets_path('odm_texturing'), f))] From 289ef48b12cc86b720b8fef60477b5d9d809f6a0 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Sat, 11 May 2024 17:08:53 -0400 Subject: [PATCH 23/29] Speed up DSM/DTM tiler --- app/api/tiler.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/api/tiler.py b/app/api/tiler.py index 83a85a0c7..aa38d1649 100644 --- a/app/api/tiler.py +++ b/app/api/tiler.py @@ -399,7 +399,7 @@ def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="", # Hillshading is not a local tile operation and # requires neighbor tiles to be rendered seamlessly if hillshade is not None: - tile_buffer = tilesize + tile_buffer = 16 try: if expr is not None: @@ -471,17 +471,17 @@ def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="", # Remove elevation data from edge buffer tiles # (to keep intensity uniform across tiles) elevation = tile.data[0] - elevation[0:tilesize, 0:tilesize] = nodata - elevation[tilesize*2:tilesize*3, 0:tilesize] = nodata - elevation[0:tilesize, tilesize*2:tilesize*3] = nodata - elevation[tilesize*2:tilesize*3, tilesize*2:tilesize*3] = nodata + elevation[0:tile_buffer, 0:tile_buffer] = nodata + elevation[tile_buffer+tilesize:tile_buffer*2+tilesize, 0:tile_buffer] = nodata + elevation[0:tile_buffer, tile_buffer+tilesize:tile_buffer*2+tilesize] = nodata + elevation[tile_buffer+tilesize:tile_buffer*2+tilesize, tile_buffer+tilesize:tile_buffer*2+tilesize] = nodata intensity = ls.hillshade(elevation, dx=dx, dy=dy, vert_exag=hillshade) - intensity = intensity[tilesize:tilesize * 2, tilesize:tilesize * 2] + intensity = intensity[tile_buffer:tile_buffer+tilesize, tile_buffer:tile_buffer+tilesize] if intensity is not None: rgb = tile.post_process(in_range=(rescale_arr,)) - rgb_data = rgb.data[:,tilesize:tilesize * 2, tilesize:tilesize * 2] + rgb_data = rgb.data[:,tile_buffer:tilesize + tile_buffer, tile_buffer:tilesize + tile_buffer] if colormap: rgb, _discard_ = apply_cmap(rgb_data, colormap.get(color_map)) if rgb.data.shape[0] != 3: @@ -490,7 +490,7 @@ def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="", intensity = intensity * 255.0 rgb = hsv_blend(rgb, intensity) if rgb is not None: - mask = tile.mask[tilesize:tilesize * 2, tilesize:tilesize * 2] + mask = tile.mask[tile_buffer:tilesize + tile_buffer, tile_buffer:tilesize + tile_buffer] return HttpResponse( render(rgb, mask, img_format=driver, **options), content_type="image/{}".format(ext) From 9ee58f721697161167e7f79ddd5f0dd91dae38eb Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Sat, 11 May 2024 17:18:54 -0400 Subject: [PATCH 24/29] Reformat --- app/api/tiler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/tiler.py b/app/api/tiler.py index aa38d1649..a57007721 100644 --- a/app/api/tiler.py +++ b/app/api/tiler.py @@ -481,7 +481,7 @@ def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="", if intensity is not None: rgb = tile.post_process(in_range=(rescale_arr,)) - rgb_data = rgb.data[:,tile_buffer:tilesize + tile_buffer, tile_buffer:tilesize + tile_buffer] + rgb_data = rgb.data[:,tile_buffer:tilesize+tile_buffer, tile_buffer:tilesize+tile_buffer] if colormap: rgb, _discard_ = apply_cmap(rgb_data, colormap.get(color_map)) if rgb.data.shape[0] != 3: @@ -490,7 +490,7 @@ def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="", intensity = intensity * 255.0 rgb = hsv_blend(rgb, intensity) if rgb is not None: - mask = tile.mask[tile_buffer:tilesize + tile_buffer, tile_buffer:tilesize + tile_buffer] + mask = tile.mask[tile_buffer:tilesize+tile_buffer, tile_buffer:tilesize+tile_buffer] return HttpResponse( render(rgb, mask, img_format=driver, **options), content_type="image/{}".format(ext) From e9c2409ea9e9156856be6ee9568f6a1cd40170e7 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Sat, 11 May 2024 17:19:32 -0400 Subject: [PATCH 25/29] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d78f5493..624a46ad7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "2.4.3", + "version": "2.5.0", "description": "User-friendly, extendable application and API for processing aerial imagery.", "main": "index.js", "scripts": { From 80a7f2048dc7b346c6cc247778a9c479537cda3d Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 13 May 2024 13:04:56 -0400 Subject: [PATCH 26/29] Potree units sync --- app/static/app/js/ModelView.jsx | 23 ++++++++++++++++++++++- app/static/app/js/classes/Units.js | 4 ++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/static/app/js/ModelView.jsx b/app/static/app/js/ModelView.jsx index 3a0478ce1..922fd83d8 100644 --- a/app/static/app/js/ModelView.jsx +++ b/app/static/app/js/ModelView.jsx @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'; import * as THREE from 'THREE'; import $ from 'jquery'; import { _, interpolate } from './classes/gettext'; +import { getUnitSystem, setUnitSystem } from './classes/Units'; require('./vendor/OBJLoader'); require('./vendor/MTLLoader'); @@ -301,6 +302,20 @@ class ModelView extends React.Component { viewer.setPointBudget(10*1000*1000); viewer.setEDLEnabled(true); viewer.loadSettingsFromURL(); + + const currentUnit = getUnitSystem(); + const origSetUnit = viewer.setLengthUnitAndDisplayUnit; + viewer.setLengthUnitAndDisplayUnit = (lengthUnit, displayUnit) => { + if (displayUnit === 'm') setUnitSystem('metric'); + else if (displayUnit === 'ft'){ + // Potree doesn't have US/international imperial, so + // we default to international unless the user has previously + // selected US + if (currentUnit === 'metric') setUnitSystem("imperial"); + else setUnitSystem(currentUnit); + } + origSetUnit.call(viewer, lengthUnit, displayUnit); + }; viewer.loadGUI(() => { viewer.setLanguage('en'); @@ -335,7 +350,7 @@ class ModelView extends React.Component { directional.position.z = 99999999999; viewer.scene.scene.add( directional ); - this.pointCloudFilePath(pointCloudPath => { + this.pointCloudFilePath(pointCloudPath =>{ Potree.loadPointCloud(pointCloudPath, "Point Cloud", e => { if (e.type == "loading_failed"){ this.setState({error: "Could not load point cloud. This task doesn't seem to have one. Try processing the task again."}); @@ -351,6 +366,12 @@ class ModelView extends React.Component { viewer.fitToScreen(); + if (getUnitSystem() === 'metric'){ + viewer.setLengthUnitAndDisplayUnit('m', 'm'); + }else{ + viewer.setLengthUnitAndDisplayUnit('m', 'ft'); + } + // Load saved scene (if any) $.ajax({ type: "GET", diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js index 6a3b665e1..d07520497 100644 --- a/app/static/app/js/classes/Units.js +++ b/app/static/app/js/classes/Units.js @@ -162,6 +162,10 @@ class ValueUnit{ } class NanUnit{ + constructor(){ + this.value = NaN; + this.unit = units.meters; // Don't matter + } toString(){ return "NaN"; } From 7ab95bc8b1eab376f6be4ff4148e0481fa319109 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 13 May 2024 13:54:49 -0400 Subject: [PATCH 27/29] Measure plugin support for imperial units --- app/static/app/js/classes/Units.js | 12 ++++++------ coreplugins/measure/public/MeasurePopup.jsx | 21 ++++++++++++++------- coreplugins/measure/public/app.jsx | 10 ++++++++++ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js index d07520497..89c4bd1b6 100644 --- a/app/static/app/js/classes/Units.js +++ b/app/static/app/js/classes/Units.js @@ -81,42 +81,42 @@ const units = { factor: 1 / (0.3048 * 0.3048), abbr: 'ft²', round: 2, - label: _("Squared Feet"), + label: _("Square Feet"), type: types.AREA }, sqfeet_us: { factor: Math.pow(3937 / 1200, 2), abbr: 'ft² (US)', round: 2, - label: _("Squared Feet"), + label: _("Square Feet"), type: types.AREA }, sqmeters: { factor: 1, abbr: 'm²', round: 2, - label: _("Squared Meters"), + label: _("Square Meters"), type: types.AREA }, sqkilometers: { factor: 0.000001, abbr: 'km²', round: 5, - label: _("Squared Kilometers"), + label: _("Square Kilometers"), type: types.AREA }, sqmiles: { factor: Math.pow((1 / 0.3048) / 5280, 2), abbr: 'mi²', round: 5, - label: _("Squared Miles"), + label: _("Square Miles"), type: types.AREA }, sqmiles_us: { factor: Math.pow((3937 / 1200) / 5280, 2), abbr: 'mi² (US)', round: 5, - label: _("Squared Miles"), + label: _("Square Miles"), type: types.AREA }, cbmeters:{ diff --git a/coreplugins/measure/public/MeasurePopup.jsx b/coreplugins/measure/public/MeasurePopup.jsx index 0e0a6a9d7..235b5f933 100644 --- a/coreplugins/measure/public/MeasurePopup.jsx +++ b/coreplugins/measure/public/MeasurePopup.jsx @@ -4,7 +4,7 @@ import './MeasurePopup.scss'; import Utils from 'webodm/classes/Utils'; import Workers from 'webodm/classes/Workers'; import { _, interpolate } from 'webodm/classes/gettext'; - +import { systems, unitSystem, getUnitSystem } from 'webodm/classes/Units'; import $ from 'jquery'; import L from 'leaflet'; @@ -50,15 +50,19 @@ export default class MeasurePopup extends React.Component { } getProperties(){ + const us = systems[this.lastUnitSystem]; + const result = { - Length: this.props.model.length, - Area: this.props.model.area + Length: us.length(this.props.model.length).value, + Area: us.area(this.props.model.area).value }; - + if (this.state.volume !== null && this.state.volume !== false){ - result.Volume = this.state.volume; + result.Volume = us.volume(this.state.volume).value; result.BaseSurface = this.state.baseMethod; } + + result.UnitSystem = this.lastUnitSystem; return result; } @@ -167,6 +171,9 @@ export default class MeasurePopup extends React.Component { render(){ const { volume, error, featureType } = this.state; + const us = unitSystem(); + this.lastUnitSystem = getUnitSystem(); + const baseMethods = [ {label: _("Triangulate"), method: 'triangulate'}, {label: _("Plane"), method: 'plane'}, @@ -175,12 +182,12 @@ export default class MeasurePopup extends React.Component { {label: _("Lowest"), method: 'lowest'}]; return (
- {featureType == "Polygon" &&

{_("Area:")} {this.props.model.areaDisplay}

} {featureType == "Polygon" &&

{_("Perimeter:")} {this.props.model.lengthDisplay}

} + {featureType == "Polygon" &&

{_("Area:")} {this.props.model.areaDisplay}

} {featureType == "Polygon" && volume === null && !error &&

{_("Volume:")} {_("computing…")}

} {typeof volume === "number" ? [ -

{_("Volume:")} {volume.toFixed("2")} {_("Cubic Meters")} ({(volume * 35.3147).toFixed(2)} {_("Cubic Feet")})

, +

{_("Volume:")} {us.volume(volume).toString()}

,

{_("Base surface:")}