From ead0636db640d5d1c6d77c245d649c07c1a98960 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 14 Nov 2025 12:42:19 -0500 Subject: [PATCH 1/7] Handle setting CRS to GCP when GCP file is selected --- app/static/app/js/components/EditTaskForm.jsx | 13 ++++++++++++- app/static/app/js/components/NewTaskPanel.jsx | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/static/app/js/components/EditTaskForm.jsx b/app/static/app/js/components/EditTaskForm.jsx index 4d76fa7a5..3426b7f1b 100644 --- a/app/static/app/js/components/EditTaskForm.jsx +++ b/app/static/app/js/components/EditTaskForm.jsx @@ -28,7 +28,8 @@ class EditTaskForm extends React.Component { inReview: PropTypes.bool, task: PropTypes.object, suggestedTaskName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - getCropPolygon: PropTypes.func + getCropPolygon: PropTypes.func, + getGcpFile: PropTypes.func }; constructor(props){ @@ -368,6 +369,16 @@ class EditTaskForm extends React.Component { } } + // If a processing node supports "crs" as an option + // and a GCP file is provided, and the user hasn't specified + // a preference, default to "gcp" (set the CRS to use the GCP's CRS) + if (this.props.getGcpFile){ + if (this.props.getGcpFile() && optionNames['crs']){ + let crsOpt = optsCopy.find(opt => opt.name === 'crs'); + if (!crsOpt) optsCopy.push({name: 'crs', value: 'gcp'}); + } + } + return optsCopy.filter(opt => optionNames[opt.name]); } diff --git a/app/static/app/js/components/NewTaskPanel.jsx b/app/static/app/js/components/NewTaskPanel.jsx index 8ba6b4c02..e7772e8d3 100644 --- a/app/static/app/js/components/NewTaskPanel.jsx +++ b/app/static/app/js/components/NewTaskPanel.jsx @@ -172,6 +172,18 @@ class NewTaskPanel extends React.Component { return this.mapPreview.getCropPolygon(); }; + getGcpFile = () => { + if (!this.props.getFiles) return null; + + const files = this.props.getFiles(); + for (let i = 0; i < files.length; i++){ + const f = files[i]; + if (f.type.indexOf("text") === 0 && ["geo.txt", "image_groups.txt"].indexOf(f.name.toLowerCase()) === -1){ + return f; + } + } + } + handlePolygonChange = () => { if (this.taskForm) this.taskForm.forceUpdate(); } @@ -236,6 +248,7 @@ class NewTaskPanel extends React.Component { inReview={this.state.inReview} suggestedTaskName={this.handleSuggestedTaskName} getCropPolygon={this.getCropPolygon} + getGcpFile={this.getGcpFile} ref={(domNode) => { if (domNode) this.taskForm = domNode; }} /> From 2de865f1fe142a13cb451f142be24818e627ea91 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 14 Nov 2025 13:53:36 -0500 Subject: [PATCH 2/7] GCP client side validation, set CRS automatically to match GCP --- app/static/app/js/classes/Gcp.js | 59 +++++++++---------- app/static/app/js/components/NewTaskPanel.jsx | 34 ++++++++++- app/templates/app/base.html | 3 + app/templates/app/dashboard.html | 2 +- app/templatetags/settings.py | 2 +- 5 files changed, 65 insertions(+), 35 deletions(-) diff --git a/app/static/app/js/classes/Gcp.js b/app/static/app/js/classes/Gcp.js index c377d30b8..4280e9056 100644 --- a/app/static/app/js/classes/Gcp.js +++ b/app/static/app/js/classes/Gcp.js @@ -1,23 +1,22 @@ +import { _, interpolate } from './gettext'; + class Gcp{ constructor(text){ - this.text = text; - } - - // Scale the image location of GPCs - // according to the values specified in the map - // @param imagesRatioMap {Object} object in which keys are image names and values are scaling ratios - // example: {'DJI_0018.jpg': 0.5, 'DJI_0019.JPG': 0.25} - // @return {Gcp} a new GCP object - resize(imagesRatioMap, muteWarnings = false){ - // Make sure dict is all lower case and values are floats - let ratioMap = {}; - for (let k in imagesRatioMap) ratioMap[k.toLowerCase()] = parseFloat(imagesRatioMap[k]); + this.crs = ""; + this.errors = []; + // this.entries = []; - const lines = this.text.split(/\r?\n/); - let output = ""; + const lines = text.split(/\r?\n/); if (lines.length > 0){ - output += lines[0] + '\n'; // coordinate system description + this.crs = lines[0]; + + // Check header + let c = this.crs.toUpperCase(); + console.log(c); + if (!c.startsWith("WGS84") && !c.startsWith("+PROJ") && !c.startsWith("EPSG:")){ + this.errors.push(interpolate(_("Invalid CRS: %(line)s"), { line: this.crs } )); + } for (let i = 1; i < lines.length; i++){ let line = lines[i].trim(); @@ -25,34 +24,30 @@ class Gcp{ let parts = line.split(/\s+/); if (parts.length >= 6){ let [x, y, z, px, py, imagename, ...extracols] = parts; - let ratio = ratioMap[imagename.toLowerCase()]; px = parseFloat(px); py = parseFloat(py); - - if (ratio !== undefined){ - px *= ratio; - py *= ratio; - }else{ - if (!muteWarnings) console.warn(`${imagename} not found in ratio map. Are you missing some images?`); + x = parseFloat(x); + y = parseFloat(y); + z = parseFloat(y); + if (isNaN(px) || isNaN(py) || isNaN(x) || isNaN(y)){ + this.errors.push(interpolate(_("Invalid line %(num)s: %(line)s"), { num: i + 1, line })); + continue; } - - let extra = extracols.length > 0 ? ' ' + extracols.join(' ') : ''; - output += `${x} ${y} ${z} ${px.toFixed(8)} ${py.toFixed(8)} ${imagename}${extra}\n`; }else{ - if (!muteWarnings) console.warn(`Invalid GCP format at line ${i}: ${line}`); - output += line + '\n'; + this.errors.push(interpolate(_("Invalid line %(num)s: %(line)s"), { num: i + 1, line })); } } } - } - return new Gcp(output); + }else{ + this.errors.push(_("Empty GCP file")); + } } - toString(){ - return this.text; + valid(){ + return this.errors.length === 0; } } -module.exports = Gcp; +export default Gcp; diff --git a/app/static/app/js/components/NewTaskPanel.jsx b/app/static/app/js/components/NewTaskPanel.jsx index e7772e8d3..3a44b9694 100644 --- a/app/static/app/js/components/NewTaskPanel.jsx +++ b/app/static/app/js/components/NewTaskPanel.jsx @@ -8,6 +8,7 @@ import MapPreview from './MapPreview'; import update from 'immutability-helper'; import PluginsAPI from '../classes/plugins/API'; import statusCodes from '../classes/StatusCodes'; +import Gcp from '../classes/Gcp'; import { _, interpolate } from '../classes/gettext'; class NewTaskPanel extends React.Component { @@ -45,6 +46,7 @@ class NewTaskPanel extends React.Component { loading: false, showMapPreview: false, dismissImageCountWarning: false, + showMalformedGcpErrors: false, }; this.save = this.save.bind(this); @@ -179,6 +181,20 @@ class NewTaskPanel extends React.Component { for (let i = 0; i < files.length; i++){ const f = files[i]; if (f.type.indexOf("text") === 0 && ["geo.txt", "image_groups.txt"].indexOf(f.name.toLowerCase()) === -1){ + if (!f._gcp){ + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target.result){ + const gcp = new Gcp(e.target.result); + if (!gcp.valid()){ + this.setState({showMalformedGcpErrors: true}); + } + f._gcp = gcp; + } + }; + reader.readAsText(f); + } + return f; } } @@ -213,11 +229,17 @@ class NewTaskPanel extends React.Component { let filesCountOk = true; if (this.taskForm && !this.taskForm.checkFilesCount(this.props.filesCount)) filesCountOk = false; + let fileCountInfo = interpolate(_("%(count)s files selected."), { count: this.props.filesCount }); + let gcp = this.getGcpFile(); + if (gcp){ + fileCountInfo = interpolate(_("%(count)s files and GCP file (%(name)s) selected."), { count: this.props.filesCount - 1, name: gcp.name }); + } + return (
-

{interpolate(_("%(count)s files selected. Please check these additional options:"), { count: this.props.filesCount})}

+

{fileCountInfo} {_("Please check these additional options:")}

{this.props.filesCount === 999 && !this.state.dismissImageCountWarning ?
@@ -225,6 +247,16 @@ class NewTaskPanel extends React.Component {
: ""} + {gcp && gcp._gcp && this.state.showMalformedGcpErrors ? +
+ +
${_("GCP File")}`, + errors: "
    " + gcp._gcp.errors.map(err => `
  • ${err}
  • `) + "
" + })}}>
+
: ""} + + {!filesCountOk ?
{interpolate(_("Number of files selected exceeds the maximum of %(count)s allowed on this processing node."), { count: this.taskForm.selectedNodeMaxImages() })} diff --git a/app/templates/app/base.html b/app/templates/app/base.html index e7a895fa7..d18c47961 100644 --- a/app/templates/app/base.html +++ b/app/templates/app/base.html @@ -123,6 +123,9 @@