diff --git a/app/plugins/templates/webpack.config.js.tmpl b/app/plugins/templates/webpack.config.js.tmpl
index e18da936..a6e362cf 100644
--- a/app/plugins/templates/webpack.config.js.tmpl
+++ b/app/plugins/templates/webpack.config.js.tmpl
@@ -84,8 +84,7 @@ module.exports = {
"PluginsAPI": "PluginsAPI",
"leaflet": "leaflet",
"ReactDOM": "ReactDOM",
- "React": "React",
- "gettext": "gettext"
+ "React": "React"
},
watchOptions: {
diff --git a/app/static/app/js/Dashboard.jsx b/app/static/app/js/Dashboard.jsx
index c4444967..d752b2c4 100644
--- a/app/static/app/js/Dashboard.jsx
+++ b/app/static/app/js/Dashboard.jsx
@@ -8,6 +8,7 @@ import {
Route
} from 'react-router-dom';
import $ from 'jquery';
+import { _ } from './classes/gettext';
class Dashboard extends React.Component {
constructor(){
@@ -22,7 +23,7 @@ class Dashboard extends React.Component {
}
addNewProject(project){
- if (!project.name) return (new $.Deferred()).reject("Name field is required");
+ if (!project.name) return (new $.Deferred()).reject(_("Name field is required"));
return $.ajax({
url: `/api/projects/`,
@@ -57,7 +58,7 @@ class Dashboard extends React.Component {
className="btn btn-primary btn-sm"
onClick={this.handleAddProject}>
- Add Project
+ {_("Add Project")}
@@ -92,7 +93,7 @@ $(function(){
// Do nothing
}
});
- return found ? "Your changes will be lost. Are you sure you want to leave?" : undefined;
+ return found ? _("Your changes will be lost. Are you sure you want to leave?") : undefined;
};
});
diff --git a/app/static/app/js/classes/plugins/API.js b/app/static/app/js/classes/plugins/API.js
index 7e3576d9..259dcc2d 100644
--- a/app/static/app/js/classes/plugins/API.js
+++ b/app/static/app/js/classes/plugins/API.js
@@ -23,7 +23,6 @@ if (!window.PluginsAPI){
'leaflet': { loader: 'globals-loader', exports: 'L' },
'ReactDOM': { loader: 'globals-loader', exports: 'ReactDOM' },
'React': { loader: 'globals-loader', exports: 'React' },
- 'gettext': { loader: 'globals-loader', exports: 'gettext' },
'SystemJS': { loader: 'globals-loader', exports: 'SystemJS' }
}
});
diff --git a/app/static/app/js/components/EditProjectDialog.jsx b/app/static/app/js/components/EditProjectDialog.jsx
index a22f74d0..2f24012e 100644
--- a/app/static/app/js/components/EditProjectDialog.jsx
+++ b/app/static/app/js/components/EditProjectDialog.jsx
@@ -1,18 +1,17 @@
import React from 'react';
-import ErrorMessage from './ErrorMessage';
import FormDialog from './FormDialog';
import PropTypes from 'prop-types';
-import $ from 'jquery';
+import { _ } from '../classes/gettext';
class EditProjectDialog extends React.Component {
static defaultProps = {
projectName: "",
projectDescr: "",
- title: "New Project",
- saveLabel: "Create Project",
- savingLabel: "Creating project...",
+ title: _("New Project"),
+ saveLabel: _("Create Project"),
+ savingLabel: _("Creating project..."),
saveIcon: "glyphicon glyphicon-plus",
- deleteWarning: "All tasks, images and models associated with this project will be permanently deleted. Are you sure you want to continue?",
+ deleteWarning: _("All tasks, images and models associated with this project will be permanently deleted. Are you sure you want to continue?"),
show: false
};
@@ -83,13 +82,13 @@ class EditProjectDialog extends React.Component {
onShow={this.onShow}
ref={(domNode) => { this.dialog = domNode; }}>
-
+
diff --git a/app/static/app/js/components/EditTaskForm.jsx b/app/static/app/js/components/EditTaskForm.jsx
index c43eb8fb..21e16dd5 100644
--- a/app/static/app/js/components/EditTaskForm.jsx
+++ b/app/static/app/js/components/EditTaskForm.jsx
@@ -25,20 +25,18 @@ class EditTaskForm extends React.Component {
onFormChanged: PropTypes.func,
inReview: PropTypes.bool,
task: PropTypes.object,
- suggestedTaskName: PropTypes.string,
+ suggestedTaskName: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
};
constructor(props){
super(props);
- this.namePlaceholder = "Task of " + (new Date()).toISOString();
-
this.state = {
error: "",
presetError: "",
presetActionPerforming: false,
-
- name: props.suggestedTaskName ? props.suggestedTaskName : (props.task !== null ? (props.task.name || "") : ""),
+ namePlaceholder: typeof props.suggestedTaskName === "string" ? props.suggestedTaskName : (props.task !== null ? (props.task.name || "") : "Task of " + (new Date()).toISOString()),
+ name: typeof props.suggestedTaskName === "string" ? props.suggestedTaskName : (props.task !== null ? (props.task.name || "") : ""),
loadedProcessingNodes: false,
loadedPresets: false,
@@ -47,7 +45,9 @@ class EditTaskForm extends React.Component {
selectedPreset: null,
presets: [],
- editingPreset: false
+ editingPreset: false,
+
+ loadingTaskName: false
};
this.handleNameChange = this.handleNameChange.bind(this);
@@ -272,6 +272,23 @@ class EditTaskForm extends React.Component {
});
}
+ loadSuggestedName = () => {
+ if (typeof this.props.suggestedTaskName === "function"){
+ this.setState({loadingTaskName: true});
+
+ this.props.suggestedTaskName().then(name => {
+ if (this.state.loadingTaskName){
+ this.setState({loadingTaskName: false, name});
+ }else{
+ // User started typing its own name
+ }
+ }).catch(e => {
+ // Do Nothing
+ this.setState({loadingTaskName: false});
+ })
+ }
+ }
+
handleSelectPreset(e){
this.selectPresetById(e.target.value);
}
@@ -284,6 +301,7 @@ class EditTaskForm extends React.Component {
componentDidMount(){
this.loadProcessingNodes();
this.loadPresets();
+ this.loadSuggestedName();
}
componentDidUpdate(prevProps, prevState){
@@ -304,7 +322,7 @@ class EditTaskForm extends React.Component {
}
handleNameChange(e){
- this.setState({name: e.target.value});
+ this.setState({name: e.target.value, loadingTaskName: false});
}
selectNodeByKey(key){
@@ -579,10 +597,13 @@ class EditTaskForm extends React.Component {
+ {this.state.loadingTaskName ?
+
+ : ""}
diff --git a/app/static/app/js/components/EditTaskPanel.jsx b/app/static/app/js/components/EditTaskPanel.jsx
index d24530b2..d15662c0 100644
--- a/app/static/app/js/components/EditTaskPanel.jsx
+++ b/app/static/app/js/components/EditTaskPanel.jsx
@@ -4,6 +4,7 @@ import ErrorMessage from './ErrorMessage';
import EditTaskForm from './EditTaskForm';
import PropTypes from 'prop-types';
import $ from 'jquery';
+import { _ } from '../classes/gettext';
class EditTaskPanel extends React.Component {
static defaultProps = {
@@ -52,7 +53,7 @@ class EditTaskPanel extends React.Component {
this.setState({saving: false});
this.props.onSave(json);
}).fail(() => {
- this.setState({saving: false, error: "Could not update task information. Plese try again."});
+ this.setState({saving: false, error: _("Could not update task information. Plese try again.")});
});
}
@@ -71,14 +72,14 @@ class EditTaskPanel extends React.Component {
task={this.props.task}
/>
-
+
diff --git a/app/static/app/js/components/FormDialog.jsx b/app/static/app/js/components/FormDialog.jsx
index e42e1c8d..e354e564 100644
--- a/app/static/app/js/components/FormDialog.jsx
+++ b/app/static/app/js/components/FormDialog.jsx
@@ -3,14 +3,15 @@ import ErrorMessage from './ErrorMessage';
import '../css/FormDialog.scss';
import PropTypes from 'prop-types';
import $ from 'jquery';
+import { _ } from '../classes/gettext';
class FormDialog extends React.Component {
static defaultProps = {
- title: "Title",
- saveLabel: "Save",
- savingLabel: "Saving...",
+ title: _("Title"),
+ saveLabel: _("Save"),
+ savingLabel: _("Saving..."),
saveIcon: "glyphicon glyphicon-plus",
- deleteWarning: "Are you sure?",
+ deleteWarning: _("Are you sure?"),
show: false
};
@@ -105,7 +106,7 @@ class FormDialog extends React.Component {
if (this.props.getFormData) formData = this.props.getFormData();
this.props.saveAction(formData).fail(e => {
- this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || "Could not apply changes"});
+ this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || _("Could not apply changes")});
}).always(() => {
this.setState({saving: false});
}).done(() => {
@@ -119,7 +120,7 @@ class FormDialog extends React.Component {
this.setState({deleting: true});
this.props.deleteAction()
.fail(e => {
- if (this._mounted) this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || "Could not delete item"});
+ if (this._mounted) this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || _("Could not delete item")});
}).always(() => {
if (this._mounted) this.setState({deleting: false});
});
@@ -147,7 +148,7 @@ class FormDialog extends React.Component {
-
+
diff --git a/app/static/app/js/components/NewTaskPanel.jsx b/app/static/app/js/components/NewTaskPanel.jsx
index eeb1c3f3..380611f9 100644
--- a/app/static/app/js/components/NewTaskPanel.jsx
+++ b/app/static/app/js/components/NewTaskPanel.jsx
@@ -19,7 +19,7 @@ class NewTaskPanel extends React.Component {
filesCount: PropTypes.number,
showResize: PropTypes.bool,
getFiles: PropTypes.func,
- suggestedTaskName: PropTypes.string,
+ suggestedTaskName: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
};
constructor(props){
diff --git a/app/static/app/js/components/ProjectList.jsx b/app/static/app/js/components/ProjectList.jsx
index d226cbb2..c2d662ce 100644
--- a/app/static/app/js/components/ProjectList.jsx
+++ b/app/static/app/js/components/ProjectList.jsx
@@ -6,7 +6,7 @@ import ProjectListItem from './ProjectListItem';
import Paginated from './Paginated';
import Paginator from './Paginator';
import ErrorMessage from './ErrorMessage';
-import { Route } from 'react-router-dom';
+import { _, interpolate } from '../classes/gettext';
import PropTypes from 'prop-types';
class ProjectList extends Paginated {
@@ -53,14 +53,14 @@ class ProjectList extends Paginated {
this.updatePagination(this.PROJECTS_PER_PAGE, json.count);
}else{
this.setState({
- error: `Invalid JSON response: ${JSON.stringify(json)}`,
+ error: interpolate(_("Invalid JSON response: %(error)s"), {error: JSON.stringify(json)}),
loading: false
});
}
})
.fail((jqXHR, textStatus, errorThrown) => {
this.setState({
- error: `Could not load projects list: ${textStatus}`,
+ error: interpolate(_("Could not load projects list: %(error)s"), {error: textStatus}),
loading: false
});
})
diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx
index d33aacae..75c7783a 100644
--- a/app/static/app/js/components/ProjectListItem.jsx
+++ b/app/static/app/js/components/ProjectListItem.jsx
@@ -12,6 +12,8 @@ import csrf from '../django/csrf';
import HistoryNav from '../classes/HistoryNav';
import PropTypes from 'prop-types';
import ResizeModes from '../classes/ResizeModes';
+import exifr from 'exifr/dist/mini.esm';
+import { _, interpolate } from '../classes/gettext';
import $ from 'jquery';
class ProjectListItem extends React.Component {
@@ -180,7 +182,7 @@ class ProjectListItem extends React.Component {
file.retries++;
this.dz.processQueue();
}else{
- throw new Error(`Cannot upload ${file.name}, exceeded max retries (${MAX_RETRIES})`);
+ throw new Error(interpolate(_('Cannot upload %(filename)s, exceeded max retries (%(max_retries)s)'), {filename: file.name, max_retries: MAX_RETRIES}));
}
};
@@ -229,17 +231,17 @@ class ProjectListItem extends React.Component {
if (task && task.id){
this.newTaskAdded();
}else{
- this.setUploadState({error: `Cannot create new task. Invalid response from server: ${JSON.stringify(task)}`});
+ this.setUploadState({error: interpolate(_('Cannot create new task. Invalid response from server: %(error)s'), { error: JSON.stringify(task) }) });
}
}).fail(() => {
- this.setUploadState({error: "Cannot create new task. Please try again later."});
+ this.setUploadState({error: _("Cannot create new task. Please try again later.")});
});
}else if (this.dz.getQueuedFiles() === 0){
// Done but didn't upload all?
this.setUploadState({
totalCount: this.state.upload.totalCount - remainingFilesCount,
uploading: false,
- error: `${remainingFilesCount} files cannot be uploaded. As a reminder, only images (.jpg, .tif, .png) and GCP files (.txt) can be uploaded. Try again.`
+ error: interpolate(_('%(count)s files cannot be uploaded. As a reminder, only images (.jpg, .tif, .png) and GCP files (.txt) can be uploaded. Try again.'), { count: remainingFilesCount })
});
}
})
@@ -341,11 +343,11 @@ class ProjectListItem extends React.Component {
this.dz.options.url = `/api/projects/${this.state.data.id}/tasks/${task.id}/upload/`;
this.dz.processQueue();
}else{
- this.setState({error: `Cannot create new task. Invalid response from server: ${JSON.stringify(task)}`});
+ this.setState({error: interpolate(_('Cannot create new task. Invalid response from server: %(error)s'), { error: JSON.stringify(task) }) });
this.handleTaskCanceled();
}
}).fail(() => {
- this.setState({error: "Cannot create new task. Please try again later."});
+ this.setState({error: _("Cannot create new task. Please try again later.")});
this.handleTaskCanceled();
});
}
@@ -393,6 +395,68 @@ class ProjectListItem extends React.Component {
this.setState({importing: false});
}
+ handleTaskTitleHint = () => {
+ return new Promise((resolve, reject) => {
+ if (this.state.upload.files.length > 0){
+
+ // Find first image in list
+ let f = null;
+ for (let i = 0; i < this.state.upload.files.length; i++){
+ if (this.state.upload.files[i].type.indexOf("image") === 0){
+ f = this.state.upload.files[i];
+ break;
+ }
+ }
+ if (!f){
+ reject();
+ return;
+ }
+
+ // Parse EXIF
+ const options = {
+ ifd0: false,
+ exif: [0x9003],
+ gps: [0x0001, 0x0002, 0x0003, 0x0004],
+ interop: false,
+ ifd1: false // thumbnail
+ };
+ exifr.parse(f, options).then(gps => {
+ if (!gps.latitude || !gps.longitude){
+ reject();
+ return;
+ }
+
+ let dateTime = gps["36867"];
+
+ // Try to parse the date from EXIF to JS
+ const parts = dateTime.split(" ");
+ if (parts.length == 2){
+ let [ d, t ] = parts;
+ d = d.replace(":", "-");
+ const tm = Date.parse(`${d} ${t}`);
+ if (!isNaN(tm)){
+ dateTime = new Date(tm).toLocaleDateString();
+ }
+ }
+
+ // Fallback to file modified date if
+ // no exif info is available
+ if (!dateTime) dateTime = f.lastModifiedDate.toLocaleDateString();
+
+ // Query nominatim OSM
+ $.ajax({
+ url: `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${gps.latitude}&lon=${gps.longitude}`,
+ contentType: 'application/json',
+ type: 'GET'
+ }).done(json => {
+ if (json.name) resolve(`${json.name} - ${dateTime}`);
+ else reject(new Error("Invalid json"));
+ }).fail(reject);
+ }).catch(reject);
+ }
+ });
+ }
+
render() {
const { refreshing, data } = this.state;
const numTasks = data.tasks.length;
@@ -405,9 +469,9 @@ class ProjectListItem extends React.Component {
{ this.editProjectDialog = domNode; }}
- title="Edit Project"
- saveLabel="Save Changes"
- savingLabel="Saving changes..."
+ title={_("Edit Project")}
+ saveLabel={_("Save Changes")}
+ savingLabel={_("Saving changes...")}
saveIcon="far fa-edit"
projectName={data.name}
projectDescr={data.description}
@@ -425,12 +489,12 @@ class ProjectListItem extends React.Component {
onClick={this.handleUpload}
ref={this.setRef("uploadButton")}>
- Select Images and GCP
+ {_("Select Images and GCP")}
{this.state.buttons.map((button, i) => {button})}
@@ -445,7 +509,7 @@ class ProjectListItem extends React.Component {
@@ -460,13 +524,13 @@ class ProjectListItem extends React.Component {