From d76eacabd3207a9641147aa481fa566db534d581 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 9 May 2024 15:46:30 -0400 Subject: [PATCH] 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}
: ""}