From baae377156a232ea892f4c6f11ca7dfd108f8e5b Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 21 Feb 2023 12:06:47 -0500 Subject: [PATCH 01/41] Sort/filter/search mockup --- .../app/js/components/ProjectListItem.jsx | 25 ++++++++++- app/static/app/js/components/SortItems.jsx | 44 +++++++++++++++++++ app/static/app/js/css/ProjectListItem.scss | 8 ++++ app/static/app/js/css/SortItems.scss | 15 +++++++ app/static/app/js/css/TaskList.scss | 3 ++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 app/static/app/js/components/SortItems.jsx create mode 100644 app/static/app/js/css/SortItems.scss diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx index 44bf3d9c..c4e479fe 100644 --- a/app/static/app/js/components/ProjectListItem.jsx +++ b/app/static/app/js/components/ProjectListItem.jsx @@ -7,6 +7,7 @@ import ImportTaskPanel from './ImportTaskPanel'; import UploadProgressBar from './UploadProgressBar'; import ErrorMessage from './ErrorMessage'; import EditProjectDialog from './EditProjectDialog'; +import SortItems from './SortItems'; import Dropzone from '../vendor/dropzone'; import csrf from '../django/csrf'; import HistoryNav from '../classes/HistoryNav'; @@ -471,6 +472,13 @@ class ProjectListItem extends React.Component { const { refreshing, data } = this.state; const numTasks = data.tasks.length; const canEdit = this.hasPermission("change"); + const sortItems = [{ + key: "created_at", + label: _("Created on") + },{ + key: "name", + label: _("Name") + }]; return (
  • 0 ? - + {interpolate(_("%(count)s Tasks"), { count: numTasks})} : ""} + + {this.state.showTaskList && numTasks > 1 ? +
    + + + {_("Filter")} + +
    + + + +
    +
    : ""} {canEdit ? [ diff --git a/app/static/app/js/components/SortItems.jsx b/app/static/app/js/components/SortItems.jsx new file mode 100644 index 00000000..6449c089 --- /dev/null +++ b/app/static/app/js/components/SortItems.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import '../css/SortItems.scss'; +import PropTypes from 'prop-types'; +import { _ } from '../classes/gettext'; + +class SortItems extends React.Component { + static defaultProps = { + items: [] + }; + + static propTypes = { + items: PropTypes.arrayOf(PropTypes.object) + }; + + constructor(props){ + super(props); + + } + + handleClick = (key, order) => { + return () => { + console.log(key, order); + } + } + + render() { + return (); + } +} + +export default SortItems; diff --git a/app/static/app/js/css/ProjectListItem.scss b/app/static/app/js/css/ProjectListItem.scss index c337e892..4e864a92 100644 --- a/app/static/app/js/css/ProjectListItem.scss +++ b/app/static/app/js/css/ProjectListItem.scss @@ -12,6 +12,10 @@ } } + .project-description{ + min-height: 12px; + } + .drag-drop-icon{ display: none; position: absolute; @@ -97,4 +101,8 @@ } } } + + .task-filters{ + float: right; + } } diff --git a/app/static/app/js/css/SortItems.scss b/app/static/app/js/css/SortItems.scss new file mode 100644 index 00000000..f8e2d872 --- /dev/null +++ b/app/static/app/js/css/SortItems.scss @@ -0,0 +1,15 @@ +.sort-items{ + .sort-order-label{ + opacity: 0.7; + padding-left: 12px; + } + + a{ + margin-right: 0 !important; + padding-left: 24px !important; + } + + a:hover{ + cursor: pointer; + } +} diff --git a/app/static/app/js/css/TaskList.scss b/app/static/app/js/css/TaskList.scss index 49ff01e5..d366de1f 100644 --- a/app/static/app/js/css/TaskList.scss +++ b/app/static/app/js/css/TaskList.scss @@ -1,2 +1,5 @@ .task-list{ + .task-bar{ + text-align: right; + } } \ No newline at end of file From c0488f5760b78b35946135427a30560a90a09dc7 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 22 Feb 2023 11:56:38 -0500 Subject: [PATCH 02/41] Task sort working --- .../app/js/components/ProjectListItem.jsx | 37 +++++++++++----- app/static/app/js/components/SortItems.jsx | 44 ------------------- app/static/app/js/components/TaskList.jsx | 2 + app/static/app/js/components/TaskListItem.jsx | 2 +- app/static/app/js/css/SortItems.scss | 15 ------- 5 files changed, 28 insertions(+), 72 deletions(-) delete mode 100644 app/static/app/js/components/SortItems.jsx delete mode 100644 app/static/app/js/css/SortItems.scss diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx index c4e479fe..af541fde 100644 --- a/app/static/app/js/components/ProjectListItem.jsx +++ b/app/static/app/js/components/ProjectListItem.jsx @@ -7,7 +7,7 @@ import ImportTaskPanel from './ImportTaskPanel'; import UploadProgressBar from './UploadProgressBar'; import ErrorMessage from './ErrorMessage'; import EditProjectDialog from './EditProjectDialog'; -import SortItems from './SortItems'; +import SortPanel from './SortPanel'; import Dropzone from '../vendor/dropzone'; import csrf from '../django/csrf'; import HistoryNav from '../classes/HistoryNav'; @@ -38,9 +38,19 @@ class ProjectListItem extends React.Component { data: props.data, refreshing: false, importing: false, - buttons: [] + buttons: [], + sortKey: "-created_at" }; + this.sortItems = [{ + key: "created_at", + label: _("Created on"), + selected: "desc" + },{ + key: "name", + label: _("Name") + }]; + this.toggleTaskList = this.toggleTaskList.bind(this); this.closeUploadError = this.closeUploadError.bind(this); this.cancelUpload = this.cancelUpload.bind(this); @@ -468,17 +478,20 @@ class ProjectListItem extends React.Component { }); } + sortChanged = key => { + if (this.taskList){ + this.setState({sortKey: key}); + setTimeout(() => { + this.taskList.refresh(); + }, 0); + } + } + render() { const { refreshing, data } = this.state; const numTasks = data.tasks.length; const canEdit = this.hasPermission("change"); - const sortItems = [{ - key: "created_at", - label: _("Created on") - },{ - key: "name", - label: _("Name") - }]; + return (
  • - + : ""} @@ -609,7 +622,7 @@ class ProjectListItem extends React.Component { {this.state.showTaskList ? { - return () => { - console.log(key, order); - } - } - - render() { - return (); - } -} - -export default SortItems; diff --git a/app/static/app/js/components/TaskList.jsx b/app/static/app/js/components/TaskList.jsx index 9672a1d6..85d44c7f 100644 --- a/app/static/app/js/components/TaskList.jsx +++ b/app/static/app/js/components/TaskList.jsx @@ -42,6 +42,8 @@ class TaskList extends React.Component { } loadTaskList(){ + this.setState({loading: true}); + this.taskListRequest = $.getJSON(this.props.source, json => { this.setState({ diff --git a/app/static/app/js/components/TaskListItem.jsx b/app/static/app/js/components/TaskListItem.jsx index 4dac1378..7da13aa7 100644 --- a/app/static/app/js/components/TaskListItem.jsx +++ b/app/static/app/js/components/TaskListItem.jsx @@ -403,7 +403,7 @@ class TaskListItem extends React.Component { render() { const task = this.state.task; - const name = task.name !== null ? task.name : interpolate(_("Task #%(number)s"), { number: task.id }); + const name = task.name !== null ? task.name : _("(unnamed)"); const imported = task.import_url !== ""; let status = statusCodes.description(task.status); diff --git a/app/static/app/js/css/SortItems.scss b/app/static/app/js/css/SortItems.scss deleted file mode 100644 index f8e2d872..00000000 --- a/app/static/app/js/css/SortItems.scss +++ /dev/null @@ -1,15 +0,0 @@ -.sort-items{ - .sort-order-label{ - opacity: 0.7; - padding-left: 12px; - } - - a{ - margin-right: 0 !important; - padding-left: 24px !important; - } - - a:hover{ - cursor: pointer; - } -} From bc8c75ac9a25b3320391f07969cb100ab1f5c846 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 22 Feb 2023 12:59:01 -0500 Subject: [PATCH 03/41] Add tags field component --- app/static/app/js/components/EditTaskForm.jsx | 22 +++++++- app/static/app/js/components/SortPanel.jsx | 55 +++++++++++++++++++ app/static/app/js/components/TagsField.jsx | 36 ++++++++++++ app/static/app/js/css/EditTaskForm.scss | 14 +++++ app/static/app/js/css/SortPanel.scss | 20 +++++++ app/static/app/js/css/TagsField.scss | 2 + 6 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 app/static/app/js/components/SortPanel.jsx create mode 100644 app/static/app/js/components/TagsField.jsx create mode 100644 app/static/app/js/css/SortPanel.scss create mode 100644 app/static/app/js/css/TagsField.scss diff --git a/app/static/app/js/components/EditTaskForm.jsx b/app/static/app/js/components/EditTaskForm.jsx index 0a5a8b19..b7ec1132 100644 --- a/app/static/app/js/components/EditTaskForm.jsx +++ b/app/static/app/js/components/EditTaskForm.jsx @@ -5,6 +5,7 @@ import EditPresetDialog from './EditPresetDialog'; import ErrorMessage from './ErrorMessage'; import PropTypes from 'prop-types'; import Storage from '../classes/Storage'; +import TagsField from './TagsField'; import $ from 'jquery'; import { _, interpolate } from '../classes/gettext'; @@ -48,7 +49,9 @@ class EditTaskForm extends React.Component { editingPreset: false, - loadingTaskName: false + loadingTaskName: false, + + showTagsField: true // TODO false }; this.handleNameChange = this.handleNameChange.bind(this); @@ -543,8 +546,19 @@ class EditTaskForm extends React.Component { ); + let tagsField = ""; + if (this.state.showTagsField){ + tagsField = (
    + +
    + +
    +
    ); + } + taskOptions = (
    + {tagsField}
    @@ -588,7 +602,7 @@ class EditTaskForm extends React.Component {
    -
    +
    {this.state.loadingTaskName ? : ""} @@ -598,6 +612,10 @@ class EditTaskForm extends React.Component { placeholder={this.state.namePlaceholder} value={this.state.name} /> + +
    {taskOptions} diff --git a/app/static/app/js/components/SortPanel.jsx b/app/static/app/js/components/SortPanel.jsx new file mode 100644 index 00000000..c7feb7ff --- /dev/null +++ b/app/static/app/js/components/SortPanel.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import '../css/SortPanel.scss'; +import PropTypes from 'prop-types'; +import { _ } from '../classes/gettext'; + +class SortPanel extends React.Component { + static defaultProps = { + items: [], + onChange: () => {} + }; + + static propTypes = { + items: PropTypes.arrayOf(PropTypes.object), + onChange: PropTypes.func + }; + + constructor(props){ + super(props); + + this.state = { + items: props.items + } + } + + handleClick = (key, order) => { + return () => { + this.state.items.forEach(i => { + i.selected = i.key === key ? order : false; + }); + this.setState({ + items: this.state.items + }) + this.props.onChange(order === "desc" ? "-" + key : key); + } + } + + render() { + return (); + } +} + +export default SortPanel; diff --git a/app/static/app/js/components/TagsField.jsx b/app/static/app/js/components/TagsField.jsx new file mode 100644 index 00000000..d6a83626 --- /dev/null +++ b/app/static/app/js/components/TagsField.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import '../css/TagsField.scss'; +import PropTypes from 'prop-types'; +import { _ } from '../classes/gettext'; + +class TagsField extends React.Component { + static defaultProps = { + }; + + static propTypes = { + }; + + constructor(props){ + super(props); + + this.state = { + } + } + + handleKeyDown = e => { + if (e.key === "Tab"){ + e.preventDefault(); + e.stopPropagation(); + // TODO: add badge + } + } + + render() { + return (
    this.inputText = domNode} + onKeyDown={this.handleKeyDown}>
    ); + } +} + +export default TagsField; diff --git a/app/static/app/js/css/EditTaskForm.scss b/app/static/app/js/css/EditTaskForm.scss index 0d321360..7d85d706 100644 --- a/app/static/app/js/css/EditTaskForm.scss +++ b/app/static/app/js/css/EditTaskForm.scss @@ -32,4 +32,18 @@ top: 15px; opacity: 0.5; } + + .name-fields{ + display: flex; + .btn.toggle-tags{ + margin-top: 2px; + margin-bottom: 2px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + input[type="text"]{ + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + } } \ No newline at end of file diff --git a/app/static/app/js/css/SortPanel.scss b/app/static/app/js/css/SortPanel.scss new file mode 100644 index 00000000..96ff3818 --- /dev/null +++ b/app/static/app/js/css/SortPanel.scss @@ -0,0 +1,20 @@ +.sort-items{ + .sort-order-label{ + opacity: 0.7; + padding-left: 12px; + } + + a{ + margin-right: 0 !important; + padding-left: 24px !important; + } + + a:hover{ + cursor: pointer; + } + + .fa-check{ + font-size: 80%; + margin-left: 8px; + } +} diff --git a/app/static/app/js/css/TagsField.scss b/app/static/app/js/css/TagsField.scss new file mode 100644 index 00000000..0afa93ae --- /dev/null +++ b/app/static/app/js/css/TagsField.scss @@ -0,0 +1,2 @@ +.tags-field{ +} From 809f6269bcb5c641fa97c982caa2b0ce10d4ec37 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 22 Feb 2023 14:51:50 -0500 Subject: [PATCH 04/41] Tag field logic --- app/static/app/css/theme.scss | 11 ++++ app/static/app/js/components/EditTaskForm.jsx | 13 ++++- app/static/app/js/components/TagsField.jsx | 52 ++++++++++++++++--- app/static/app/js/css/TagsField.scss | 32 ++++++++++++ 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/app/static/app/css/theme.scss b/app/static/app/css/theme.scss index bbcaa07f..34ec8729 100644 --- a/app/static/app/css/theme.scss +++ b/app/static/app/css/theme.scss @@ -261,4 +261,15 @@ pre.prettyprint, color: complementary(theme("secondary")) !important; } } +} + +.tag-badge{ + background-color: theme("button_default"); + border-color: theme("button_default"); + color: theme("secondary"); + + + a, a:hover{ + color: theme("secondary"); + } } \ No newline at end of file diff --git a/app/static/app/js/components/EditTaskForm.jsx b/app/static/app/js/components/EditTaskForm.jsx index b7ec1132..4a9c51c1 100644 --- a/app/static/app/js/components/EditTaskForm.jsx +++ b/app/static/app/js/components/EditTaskForm.jsx @@ -488,6 +488,15 @@ class EditTaskForm extends React.Component { } } + toggleTagsField = () => { + if (!this.state.showTagsField){ + setTimeout(() => { + if (this.tagsField) this.tagsField.focus(); + }, 0); + } + this.setState({showTagsField: !this.state.showTagsField}); + } + render() { if (this.state.error){ return (
    @@ -551,7 +560,7 @@ class EditTaskForm extends React.Component { tagsField = (
    - + this.tagsField = domNode}/>
    ); } @@ -612,7 +621,7 @@ class EditTaskForm extends React.Component { placeholder={this.state.namePlaceholder} value={this.state.name} /> - diff --git a/app/static/app/js/components/TagsField.jsx b/app/static/app/js/components/TagsField.jsx index d6a83626..0b765650 100644 --- a/app/static/app/js/components/TagsField.jsx +++ b/app/static/app/js/components/TagsField.jsx @@ -1,35 +1,75 @@ import React from 'react'; import '../css/TagsField.scss'; import PropTypes from 'prop-types'; +import update from 'immutability-helper'; import { _ } from '../classes/gettext'; class TagsField extends React.Component { static defaultProps = { + tags: ["abc"] }; static propTypes = { + tags: PropTypes.arrayOf(PropTypes.string) }; constructor(props){ super(props); this.state = { + tags: props.tags } } handleKeyDown = e => { - if (e.key === "Tab"){ + if (e.key === "Tab" || e.key === "Enter"){ e.preventDefault(); e.stopPropagation(); - // TODO: add badge + this.addTag(); + } + } + + focus = () => { + this.inputText.focus(); + } + + stop = e => { + e.stopPropagation(); + } + + removeTag = idx => { + return e => { + e.stopPropagation(); + + // TODO + } + } + + addTag = () => { + const text = this.inputText.innerText; + if (text !== ""){ + // Check for dulicates + if (this.state.tags.indexOf(text) === -1){ + this.setState(update(this.state, { + tags: {$push: [text]} + })); + } + this.inputText.innerText = ""; } } render() { - return (
    this.inputText = domNode} - onKeyDown={this.handleKeyDown}>
    ); + return (
    {this.state.tags.map((tag, i) => +
    {tag} ×  
    + )} +
    this.inputText = domNode} + onKeyDown={this.handleKeyDown} + onBlur={this.addTag}>
    +
    ); } } diff --git a/app/static/app/js/css/TagsField.scss b/app/static/app/js/css/TagsField.scss index 0afa93ae..6c26aa65 100644 --- a/app/static/app/js/css/TagsField.scss +++ b/app/static/app/js/css/TagsField.scss @@ -1,2 +1,34 @@ .tags-field{ + height: auto; + &:hover{ + cursor: text; + } + .tag-badge{ + &:hover{ + cursor: default; + } + display: inline-block; + width: auto; + padding-left: 6px; + padding-top: 2px; + padding-bottom: 2px; + margin-top: -2px; + border-radius: 6px; + margin-right: 4px; + + a{ + margin-top: 2px; + font-weight: bold; + } + a:hover, a:focus, a:active{ + cursor: pointer; + text-decoration: none !important; + } + } + .inputText{ + display: inline-block; + outline: none; + border: none; + } + } From e6c423f2406b741bb5f5d93a9100271f6847b946 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 22 Feb 2023 14:59:39 -0500 Subject: [PATCH 05/41] Delete tags logic --- app/static/app/js/components/TagsField.jsx | 2 +- app/static/app/js/css/TagsField.scss | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/static/app/js/components/TagsField.jsx b/app/static/app/js/components/TagsField.jsx index 0b765650..fd29e55a 100644 --- a/app/static/app/js/components/TagsField.jsx +++ b/app/static/app/js/components/TagsField.jsx @@ -41,7 +41,7 @@ class TagsField extends React.Component { return e => { e.stopPropagation(); - // TODO + this.setState(update(this.state, { tags: { $splice: [[idx, 1]] } })); } } diff --git a/app/static/app/js/css/TagsField.scss b/app/static/app/js/css/TagsField.scss index 6c26aa65..076dbcf1 100644 --- a/app/static/app/js/css/TagsField.scss +++ b/app/static/app/js/css/TagsField.scss @@ -1,5 +1,7 @@ .tags-field{ height: auto; + padding-bottom: 2px; + &:hover{ cursor: text; } @@ -13,8 +15,10 @@ padding-top: 2px; padding-bottom: 2px; margin-top: -2px; - border-radius: 6px; margin-right: 4px; + margin-bottom: 8px; + border-radius: 6px; + a{ margin-top: 2px; @@ -29,6 +33,8 @@ display: inline-block; outline: none; border: none; + margin-bottom: 10px; + min-width: 1px; } } From de79e1b606beb72bbec3ebc3f7b53e8952dd0cda Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 23 Feb 2023 15:13:00 -0500 Subject: [PATCH 06/41] Tag component drag reorder --- app/static/app/js/components/EditTaskForm.jsx | 4 +- app/static/app/js/components/TagsField.jsx | 140 +++++++++++++++++- app/static/app/js/components/TaskList.jsx | 4 +- app/static/app/js/css/EditTaskForm.scss | 2 +- app/static/app/js/css/TagsField.scss | 2 +- app/static/app/js/vendor/dropzone.js | 16 ++ 6 files changed, 157 insertions(+), 11 deletions(-) diff --git a/app/static/app/js/components/EditTaskForm.jsx b/app/static/app/js/components/EditTaskForm.jsx index 4a9c51c1..0609b50f 100644 --- a/app/static/app/js/components/EditTaskForm.jsx +++ b/app/static/app/js/components/EditTaskForm.jsx @@ -621,8 +621,8 @@ class EditTaskForm extends React.Component { placeholder={this.state.namePlaceholder} value={this.state.name} /> -
    diff --git a/app/static/app/js/components/TagsField.jsx b/app/static/app/js/components/TagsField.jsx index fd29e55a..c9140eab 100644 --- a/app/static/app/js/components/TagsField.jsx +++ b/app/static/app/js/components/TagsField.jsx @@ -6,7 +6,7 @@ import { _ } from '../classes/gettext'; class TagsField extends React.Component { static defaultProps = { - tags: ["abc"] + tags: ["abc", "123", "xyz", "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbb", "ccccccccccc", "dddddddddddd"] }; static propTypes = { @@ -19,6 +19,36 @@ class TagsField extends React.Component { this.state = { tags: props.tags } + + this.dzList = []; + this.domTags = []; + } + + componentWillUnmount(){ + this.restoreDropzones(); + } + + disableDropzones(){ + if (this.disabledDz) return; + let parent = this.domNode.parentElement; + while(parent){ + if (parent.dropzone){ + parent.dropzone.removeListeners(); + this.dzList.push(parent.dropzone); + } + parent = parent.parentElement; + } + this.disabledDz = true; + } + + restoreDropzones(){ + if (!this.disabledDz) return; + + this.dzList.forEach(dz => { + dz.restoreListeners(); + }); + this.dzList = []; + this.disabledDz = false; } handleKeyDown = e => { @@ -58,15 +88,115 @@ class TagsField extends React.Component { } } + handleDragStart = tag => { + return e => { + this.disableDropzones(); + e.stopPropagation(); + e.dataTransfer.setData("application/tag", tag); + e.dataTransfer.dropEffect = "move"; + } + } + + handleDrop = e => { + e.preventDefault(); + const dragTag = e.dataTransfer.getData("application/tag"); + const [moveTag, side] = this.findClosestTag(e.clientX, e.clientY); + + const { tags } = this.state; + if (moveTag){ + const dragIdx = tags.indexOf(dragTag); + const moveIdx = tags.indexOf(moveTag); + if (dragIdx !== -1 && moveIdx !== -1){ + if (dragIdx === moveIdx) return; + else{ + // Put drag tag in front of move tag + let insertIdx = side === "right" ? moveIdx + 1 : moveIdx; + tags.splice(insertIdx, 0, dragTag); + for (let i = 0; i < tags.length; i++){ + if (tags[i] === dragTag && i !== insertIdx){ + tags.splice(i, 1); + break; + } + } + this.setState({tags}); + } + } + } + } + handleDragOver = e => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + } + handleDragEnter = e => { + e.preventDefault(); + } + handleDragEnd = () => { + this.restoreDropzones(); + } + + findClosestTag = (clientX, clientY) => { + let closestTag = null; + let minDistX = Infinity, minDistY = Infinity; + let rowTagY = null; + const { tags } = this.state; + const row = []; + + // Find tags in closest row + this.domTags.forEach((domTag, i) => { + const b = domTag.getBoundingClientRect(); + const tagY = b.y + (b.height / 2); + let dy = clientY - tagY, + sqDistY = dy*dy; + + if (sqDistY < minDistY){ + minDistY = sqDistY; + rowTagY = tagY; + } + }); + + if (!rowTagY) return [null, ""]; + + // From row, find closest in X + this.domTags.forEach((domTag, i) => { + const b = domTag.getBoundingClientRect(); + const tagY = b.y + (b.height / 2); + if (Math.abs(tagY - rowTagY) < 0.001){ + const tagX = b.x + b.width; + let dx = clientX - tagX, + sqDistX = dx*dx; + if (sqDistX < minDistX){ + closestTag = tags[i]; + minDistX = sqDistX; + } + } + }); + + let side = "right"; + if (closestTag){ + const b = this.domTags[this.state.tags.indexOf(closestTag)].getBoundingClientRect(); + const centerX = b.x + b.width / 2.0; + if (clientX < centerX) side = "left"; + } + + return [closestTag, side]; + } + render() { return (
    this.domNode = domNode} + spellCheck="false" + autoComplete="off" onClick={this.focus} + onDrop={this.handleDrop} + onDragOver={this.handleDragOver} + onDragEnter={this.handleDragEnter} className="form-control tags-field">{this.state.tags.map((tag, i) => -
    {tag} ×  
    +
    this.domTags[i] = domNode} + onClick={this.stop} + onDragStart={this.handleDragStart(tag)} + onDragEnd={this.handleDragEnd}>{tag} ×  
    )} -
    this.inputText = domNode} +
    this.inputText = domNode} onKeyDown={this.handleKeyDown} onBlur={this.addTag}>
    ); diff --git a/app/static/app/js/components/TaskList.jsx b/app/static/app/js/components/TaskList.jsx index 85d44c7f..f855b050 100644 --- a/app/static/app/js/components/TaskList.jsx +++ b/app/static/app/js/components/TaskList.jsx @@ -90,8 +90,6 @@ class TaskList extends React.Component { return (
    - {message} - {this.state.tasks.map(task => ( ))} + + {message}
    ); } diff --git a/app/static/app/js/css/EditTaskForm.scss b/app/static/app/js/css/EditTaskForm.scss index 7d85d706..1b4966fb 100644 --- a/app/static/app/js/css/EditTaskForm.scss +++ b/app/static/app/js/css/EditTaskForm.scss @@ -28,7 +28,7 @@ .name-loading{ position: absolute; - right: 30px; + right: 60px; top: 15px; opacity: 0.5; } diff --git a/app/static/app/js/css/TagsField.scss b/app/static/app/js/css/TagsField.scss index 076dbcf1..00d4770f 100644 --- a/app/static/app/js/css/TagsField.scss +++ b/app/static/app/js/css/TagsField.scss @@ -7,7 +7,7 @@ } .tag-badge{ &:hover{ - cursor: default; + cursor: grab; } display: inline-block; width: auto; diff --git a/app/static/app/js/vendor/dropzone.js b/app/static/app/js/vendor/dropzone.js index ab1f5249..0beabbb3 100644 --- a/app/static/app/js/vendor/dropzone.js +++ b/app/static/app/js/vendor/dropzone.js @@ -1555,6 +1555,22 @@ var Dropzone = function (_Emitter) { return _this4.cancelUpload(file); }); } + }, { + key: "removeListeners", + value: function disable() { + this.clickableElements.forEach(function (element) { + return element.classList.remove("dz-clickable"); + }); + this.removeEventListeners(); + } + }, { + key: "restoreListeners", + value: function disable() { + this.clickableElements.forEach(function (element) { + return element.classList.add("dz-clickable"); + }); + return this.setupEventListeners(); + } }, { key: "enable", value: function enable() { From 8df0e9a96e63da0b5a035cc3667bce8f3b306ff9 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 23 Feb 2023 16:24:52 -0500 Subject: [PATCH 07/41] PoC search endpoint --- app/api/projects.py | 15 +++++++++++++++ app/static/app/js/components/TagsField.jsx | 3 +-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/api/projects.py b/app/api/projects.py index c86efeb9..4fc8f24b 100644 --- a/app/api/projects.py +++ b/app/api/projects.py @@ -3,8 +3,10 @@ from rest_framework import serializers, viewsets from rest_framework.decorators import action from rest_framework.response import Response from rest_framework import status +from django_filters import rest_framework as filters from django.db import transaction from django.contrib.auth.models import User +from django.db.models import Q from app import models from .tasks import TaskIDsSerializer @@ -34,6 +36,18 @@ class ProjectSerializer(serializers.ModelSerializer): exclude = ('deleting', ) +class ProjectFilter(filters.FilterSet): + search = filters.CharFilter(method='filter_search') + + def filter_search(self, queryset, name, value): + print(name, value) + return queryset.filter(Q(name__icontains=value) | Q(task__name__icontains=value)).distinct() + + class Meta: + model = models.Project + fields = ['search'] + + class ProjectViewSet(viewsets.ModelViewSet): """ Project get/add/delete/update @@ -45,6 +59,7 @@ class ProjectViewSet(viewsets.ModelViewSet): filter_fields = ('id', 'name', 'description', 'created_at') serializer_class = ProjectSerializer queryset = models.Project.objects.prefetch_related('task_set').filter(deleting=False).order_by('-created_at') + filterset_class = ProjectFilter ordering_fields = '__all__' # Disable pagination when not requesting any page diff --git a/app/static/app/js/components/TagsField.jsx b/app/static/app/js/components/TagsField.jsx index c9140eab..d7b32dfd 100644 --- a/app/static/app/js/components/TagsField.jsx +++ b/app/static/app/js/components/TagsField.jsx @@ -52,7 +52,7 @@ class TagsField extends React.Component { } handleKeyDown = e => { - if (e.key === "Tab" || e.key === "Enter"){ + if (e.key === "Tab" || e.key === "Enter" || e.key === ","){ e.preventDefault(); e.stopPropagation(); this.addTag(); @@ -139,7 +139,6 @@ class TagsField extends React.Component { let minDistX = Infinity, minDistY = Infinity; let rowTagY = null; const { tags } = this.state; - const row = []; // Find tags in closest row this.domTags.forEach((domTag, i) => { From a7b09ee3fa9266c98ab5fb5abfd1ea8ead238a36 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 7 Mar 2023 11:56:17 -0500 Subject: [PATCH 08/41] Tags persistence, system tags, sort by tag --- app/api/tags.py | 8 +++ app/api/tasks.py | 2 + app/migrations/0033_auto_20230307_1532.py | 23 ++++++++ app/models/project.py | 3 +- app/models/task.py | 3 +- app/static/app/js/classes/Tags.js | 23 ++++++++ app/static/app/js/components/EditTaskForm.jsx | 14 +++-- .../app/js/components/ProjectListItem.jsx | 3 + app/static/app/js/components/TagsField.jsx | 55 +++++++++++-------- app/static/app/js/components/TaskListItem.jsx | 7 ++- app/static/app/js/css/TagsField.scss | 2 +- app/static/app/js/css/TaskListItem.scss | 19 +++++++ 12 files changed, 130 insertions(+), 32 deletions(-) create mode 100644 app/api/tags.py create mode 100644 app/migrations/0033_auto_20230307_1532.py create mode 100644 app/static/app/js/classes/Tags.js diff --git a/app/api/tags.py b/app/api/tags.py new file mode 100644 index 00000000..c657344e --- /dev/null +++ b/app/api/tags.py @@ -0,0 +1,8 @@ +from rest_framework import serializers + +class TagsField(serializers.JSONField): + def to_representation(self, tags): + return [t for t in tags.split(" ") if t != ""] + + def to_internal_value(self, tags): + return " ".join([t.strip() for t in tags]) \ No newline at end of file diff --git a/app/api/tasks.py b/app/api/tasks.py index e1b8d3ba..2cd694ab 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -20,6 +20,7 @@ from nodeodm import status_codes from nodeodm.models import ProcessingNode from worker import tasks as worker_tasks from .common import get_and_check_project, get_asset_download_filename +from .tags import TagsField from app.security import path_traversal_check from django.utils.translation import gettext_lazy as _ @@ -41,6 +42,7 @@ class TaskSerializer(serializers.ModelSerializer): processing_node_name = serializers.SerializerMethodField() can_rerun_from = serializers.SerializerMethodField() statistics = serializers.SerializerMethodField() + tags = TagsField() def get_processing_node_name(self, obj): if obj.processing_node is not None: diff --git a/app/migrations/0033_auto_20230307_1532.py b/app/migrations/0033_auto_20230307_1532.py new file mode 100644 index 00000000..df180c60 --- /dev/null +++ b/app/migrations/0033_auto_20230307_1532.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.27 on 2023-03-07 15:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0032_task_epsg'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='tags', + field=models.TextField(blank=True, db_index=True, default='', help_text='Project tags', verbose_name='Tags'), + ), + migrations.AddField( + model_name='task', + name='tags', + field=models.TextField(blank=True, db_index=True, default='', help_text='Task tags', verbose_name='Tags'), + ), + ] diff --git a/app/models/project.py b/app/models/project.py index e3a56253..1d316b45 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -25,7 +25,8 @@ class Project(models.Model): description = models.TextField(default="", blank=True, help_text=_("More in-depth description of the project"), verbose_name=_("Description")) created_at = models.DateTimeField(default=timezone.now, help_text=_("Creation date"), verbose_name=_("Created at")) deleting = models.BooleanField(db_index=True, default=False, help_text=_("Whether this project has been marked for deletion. Projects that have running tasks need to wait for tasks to be properly cleaned up before they can be deleted."), verbose_name=_("Deleting")) - + tags = models.TextField(db_index=True, default="", blank=True, help_text=_("Project tags"), verbose_name=_("Tags")) + def delete(self, *args): # No tasks? if self.task_set.count() == 0: diff --git a/app/models/task.py b/app/models/task.py index bac6fced..79e4092a 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -276,7 +276,8 @@ class Task(models.Model): partial = models.BooleanField(default=False, help_text=_("A flag indicating whether this task is currently waiting for information or files to be uploaded before being considered for processing."), verbose_name=_("Partial")) potree_scene = fields.JSONField(default=dict, blank=True, help_text=_("Serialized potree scene information used to save/load measurements and camera view angle"), verbose_name=_("Potree Scene")) epsg = models.IntegerField(null=True, default=None, blank=True, help_text=_("EPSG code of the dataset (if georeferenced)"), verbose_name="EPSG") - + tags = models.TextField(db_index=True, default="", blank=True, help_text=_("Task tags"), verbose_name=_("Tags")) + class Meta: verbose_name = _("Task") verbose_name_plural = _("Tasks") diff --git a/app/static/app/js/classes/Tags.js b/app/static/app/js/classes/Tags.js new file mode 100644 index 00000000..5f0a58b2 --- /dev/null +++ b/app/static/app/js/classes/Tags.js @@ -0,0 +1,23 @@ +export default { + userTags: function(tags){ + // Tags starting with a "_" are considered hidden or system tags + // and should not be displayed to end users via the UI + if (Array.isArray(tags)){ + return tags.filter(t => !t.startsWith("_")); + }else return []; + }, + + systemTags: function(tags){ + // Tags starting with a "_" are considered hidden or system tags + // and should not be displayed to end users via the UI + if (Array.isArray(tags)){ + return tags.filter(t => t.startsWith("_")); + }else return []; + }, + + combine: function(user, system){ + if (Array.isArray(user) && Array.isArray(system)){ + return user.concat(system); + }else throw Error("Invalid parameters"); + } +} \ No newline at end of file diff --git a/app/static/app/js/components/EditTaskForm.jsx b/app/static/app/js/components/EditTaskForm.jsx index 0609b50f..c6e2da85 100644 --- a/app/static/app/js/components/EditTaskForm.jsx +++ b/app/static/app/js/components/EditTaskForm.jsx @@ -46,12 +46,13 @@ class EditTaskForm extends React.Component { processingNodes: [], selectedPreset: null, presets: [], + tags: Utils.clone(props.task.tags), editingPreset: false, loadingTaskName: false, - showTagsField: true // TODO false + showTagsField: !!props.task.tags.length }; this.handleNameChange = this.handleNameChange.bind(this); @@ -357,12 +358,13 @@ class EditTaskForm extends React.Component { } getTaskInfo(){ - const { name, selectedNode, selectedPreset } = this.state; + const { name, selectedNode, selectedPreset, tags } = this.state; return { name: name !== "" ? name : this.namePlaceholder, selectedNode: selectedNode, - options: this.getAvailableOptionsOnly(selectedPreset.options, selectedNode.options) + options: this.getAvailableOptionsOnly(selectedPreset.options, selectedNode.options), + tags }; } @@ -559,8 +561,8 @@ class EditTaskForm extends React.Component { if (this.state.showTagsField){ tagsField = (
    -
    - this.tagsField = domNode}/> +
    + this.state.tags = tags } tags={this.state.tags} ref={domNode => this.tagsField = domNode}/>
    ); } @@ -619,7 +621,7 @@ class EditTaskForm extends React.Component { onChange={this.handleNameChange} className="form-control" placeholder={this.state.namePlaceholder} - value={this.state.name} + value={this.state.name} /> ] : undefined} ref={(domNode) => { this.dialog = domNode; }}> -
    +
    -
    +
    { this.nameInput = domNode; }} value={this.state.name} onChange={this.handleChange('name')} onKeyPress={e => this.dialog.handleEnter(e)} /> +
    + {tagsField}
    diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx index 55e92821..8e8db2bb 100644 --- a/app/static/app/js/components/ProjectListItem.jsx +++ b/app/static/app/js/components/ProjectListItem.jsx @@ -13,6 +13,7 @@ import csrf from '../django/csrf'; import HistoryNav from '../classes/HistoryNav'; import PropTypes from 'prop-types'; import ResizeModes from '../classes/ResizeModes'; +import Tags from '../classes/Tags'; import exifr from '../vendor/exifr'; import { _, interpolate } from '../classes/gettext'; import $ from 'jquery'; @@ -397,6 +398,7 @@ class ProjectListItem extends React.Component { data: JSON.stringify({ name: project.name, description: project.descr, + tags: project.tags, permissions: project.permissions }), dataType: 'json', @@ -494,7 +496,7 @@ class ProjectListItem extends React.Component { const { refreshing, data } = this.state; const numTasks = data.tasks.length; const canEdit = this.hasPermission("change"); - + const userTags = Tags.userTags(data.tags); return (
  • {data.name} + {userTags.length > 0 ? + userTags.map((t, i) =>
    {t}
    ) + : ""}
    {data.description} diff --git a/app/static/app/js/components/TagsField.jsx b/app/static/app/js/components/TagsField.jsx index 121ed223..ca1842e5 100644 --- a/app/static/app/js/components/TagsField.jsx +++ b/app/static/app/js/components/TagsField.jsx @@ -64,6 +64,8 @@ class TagsField extends React.Component { e.preventDefault(); e.stopPropagation(); this.addTag(); + }else if (e.key === "Backspace" && this.inputText.innerText === ""){ + this.removeTag(this.state.userTags.length - 1); } } @@ -75,19 +77,26 @@ class TagsField extends React.Component { e.stopPropagation(); } - removeTag = idx => { + handleRemoveTag = idx => { return e => { e.stopPropagation(); - - this.setState(update(this.state, { userTags: { $splice: [[idx, 1]] } })); + this.removeTag(idx); } } + removeTag = idx => { + this.setState(update(this.state, { userTags: { $splice: [[idx, 1]] } })); + } + addTag = () => { - const text = this.inputText.innerText; + let text = this.inputText.innerText; if (text !== ""){ // Do not allow system tags if (!text.startsWith("_")){ + + // Only lower case text allowed + text = text.toLowerCase(); + // Check for dulicates if (this.state.userTags.indexOf(text) === -1){ this.setState(update(this.state, { @@ -204,7 +213,7 @@ class TagsField extends React.Component {
    this.domTags[i] = domNode} onClick={this.stop} onDragStart={this.handleDragStart(tag)} - onDragEnd={this.handleDragEnd}>{tag} ×  
    + onDragEnd={this.handleDragEnd}>{tag} ×  
    )}
    this.inputText = domNode} onKeyDown={this.handleKeyDown} diff --git a/app/static/app/js/css/EditProjectDialog.scss b/app/static/app/js/css/EditProjectDialog.scss new file mode 100644 index 00000000..bc7f4aeb --- /dev/null +++ b/app/static/app/js/css/EditProjectDialog.scss @@ -0,0 +1,15 @@ +.edit-project-dialog{ + .name-fields{ + display: flex; + .btn.toggle-tags{ + margin-top: 0; + margin-bottom: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + input[type="text"]{ + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + } +} \ No newline at end of file diff --git a/app/static/app/js/css/ProjectListItem.scss b/app/static/app/js/css/ProjectListItem.scss index 4e864a92..4ae129cc 100644 --- a/app/static/app/js/css/ProjectListItem.scss +++ b/app/static/app/js/css/ProjectListItem.scss @@ -105,4 +105,19 @@ .task-filters{ float: right; } + + .tag-badge.small-badge { + display: inline-block; + width: auto; + padding-left: 6px; + padding-right: 6px; + padding-top: 0px; + padding-bottom: 0px; + margin-left: 4px; + margin-top: -2px; + border-radius: 6px; + font-size: 90%; + position: relative; + top: -1px; + } } From c2c06e6d2614af3613a19f6e0a0aca11faf050fb Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 7 Mar 2023 14:10:29 -0500 Subject: [PATCH 10/41] Use dots for system tags --- app/static/app/js/classes/Tags.js | 8 +++----- app/static/app/js/components/TagsField.jsx | 2 +- app/templates/app/dashboard.html | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/static/app/js/classes/Tags.js b/app/static/app/js/classes/Tags.js index 5f0a58b2..d58cdca6 100644 --- a/app/static/app/js/classes/Tags.js +++ b/app/static/app/js/classes/Tags.js @@ -1,17 +1,15 @@ export default { userTags: function(tags){ - // Tags starting with a "_" are considered hidden or system tags + // Tags starting with a "." are considered hidden or system tags // and should not be displayed to end users via the UI if (Array.isArray(tags)){ - return tags.filter(t => !t.startsWith("_")); + return tags.filter(t => !t.startsWith(".")); }else return []; }, systemTags: function(tags){ - // Tags starting with a "_" are considered hidden or system tags - // and should not be displayed to end users via the UI if (Array.isArray(tags)){ - return tags.filter(t => t.startsWith("_")); + return tags.filter(t => t.startsWith(".")); }else return []; }, diff --git a/app/static/app/js/components/TagsField.jsx b/app/static/app/js/components/TagsField.jsx index ca1842e5..c798ff97 100644 --- a/app/static/app/js/components/TagsField.jsx +++ b/app/static/app/js/components/TagsField.jsx @@ -92,7 +92,7 @@ class TagsField extends React.Component { let text = this.inputText.innerText; if (text !== ""){ // Do not allow system tags - if (!text.startsWith("_")){ + if (!text.startsWith(".")){ // Only lower case text allowed text = text.toLowerCase(); diff --git a/app/templates/app/dashboard.html b/app/templates/app/dashboard.html index 40b30f25..1abb2f92 100644 --- a/app/templates/app/dashboard.html +++ b/app/templates/app/dashboard.html @@ -33,7 +33,7 @@
    • {% trans 'You need at least 5 images, but 16-32 is typically the minimum.' %}
    • {% trans 'Images must overlap by 65% or more. Aim for 70-72%' %}
    • -
    • {% trans 'For great 3D, images must overlap by 83%' %}
    • +
    • {% trans 'For great 3D, images must overlap by 83%' %}
    • {% blocktrans with link_start='' link_end='' %}A {{link_start}}GCP File{{link_end}} is optional, but can increase georeferencing accuracy{% endblocktrans %}

    From d46058c0420859da9e06690e5e2f9c0e83a6e686 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 8 Mar 2023 12:21:23 -0500 Subject: [PATCH 11/41] Add toolbar mockup --- app/static/app/css/theme.scss | 2 +- app/static/app/js/components/EditTaskForm.jsx | 4 +- app/static/app/js/components/Paginator.jsx | 73 ++++++++++++------- app/static/app/js/components/ProjectList.jsx | 15 +++- .../app/js/components/ProjectListItem.jsx | 12 ++- app/static/app/js/css/Paginator.scss | 8 ++ 6 files changed, 78 insertions(+), 36 deletions(-) create mode 100644 app/static/app/js/css/Paginator.scss diff --git a/app/static/app/css/theme.scss b/app/static/app/css/theme.scss index 34ec8729..d6170d9b 100644 --- a/app/static/app/css/theme.scss +++ b/app/static/app/css/theme.scss @@ -188,7 +188,7 @@ footer, border-bottom-color: theme("border"); } .theme-border{ - border-color: theme("border"); + border-color: theme("border") !important; } /* Highlight */ diff --git a/app/static/app/js/components/EditTaskForm.jsx b/app/static/app/js/components/EditTaskForm.jsx index c6e2da85..40d053d7 100644 --- a/app/static/app/js/components/EditTaskForm.jsx +++ b/app/static/app/js/components/EditTaskForm.jsx @@ -527,10 +527,10 @@ class EditTaskForm extends React.Component { {!this.state.presetActionPerforming ?
    - -
      diff --git a/app/static/app/js/components/Paginator.jsx b/app/static/app/js/components/Paginator.jsx index d2042981..8f69d6cf 100644 --- a/app/static/app/js/components/Paginator.jsx +++ b/app/static/app/js/components/Paginator.jsx @@ -1,44 +1,63 @@ import React from 'react'; +import '../css/Paginator.scss'; import { Link } from 'react-router-dom'; +import { _ } from '../classes/gettext'; class Paginator extends React.Component { + constructor(props){ + super(props); + + this.state = { + showSearch: false + } + } + + toggleSearch = () => { + + } + render() { const { itemsPerPage, totalItems, currentPage } = this.props; let paginator = null; + let toolbar = ( +
        +
      • +
      • +
      • +
      + ); if (itemsPerPage && itemsPerPage && totalItems > itemsPerPage){ const numPages = Math.ceil(totalItems / itemsPerPage), pages = [...Array(numPages).keys()]; // [0, 1, 2, ...numPages] - + paginator = ( -
      -
        -
      • - - « - -
      • - {pages.map(page => { - return (
      • - +
      • + + « + +
      • + {pages.map(page => { + return (
      • + {toolbar}{paginator}
      , + this.props.children, +
      {paginator}
      , + ]; } } diff --git a/app/static/app/js/components/ProjectList.jsx b/app/static/app/js/components/ProjectList.jsx index bbe6c1be..502c4722 100644 --- a/app/static/app/js/components/ProjectList.jsx +++ b/app/static/app/js/components/ProjectList.jsx @@ -21,7 +21,8 @@ class ProjectList extends Paginated { loading: true, refreshing: false, error: "", - projects: [] + projects: [], + showSearch: false } this.PROJECTS_PER_PAGE = 10; @@ -95,13 +96,23 @@ class ProjectList extends Paginated { this.refresh(); } + toggleSearch = (e) => { + this.setState({showSearch: !this.state.showSearch}); + } + + search = () => { + + } + + render() { if (this.state.loading){ return (
      ); }else{ + let test = (); return (
      - +
        {this.state.projects.map(p => ( Cancel Upload - -
      @@ -591,11 +587,19 @@ class ProjectListItem extends React.Component {
    : ""} + {numTasks > 0 ? + [ + , + {_("View Map")} + ] + : ""} + {canEdit ? [ , {_("Edit")} ] : ""} +
    diff --git a/app/static/app/js/css/Paginator.scss b/app/static/app/js/css/Paginator.scss new file mode 100644 index 00000000..1c5bb9c9 --- /dev/null +++ b/app/static/app/js/css/Paginator.scss @@ -0,0 +1,8 @@ +.paginator{ + .toolbar{ + i{ + opacity: 0.8; + } + margin-right: 8px; + } +} \ No newline at end of file From 70386c7ce671dbc23d5f6b7d45c102215387a583 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 8 Mar 2023 14:31:46 -0500 Subject: [PATCH 12/41] Project sort working --- app/static/app/js/Dashboard.jsx | 9 ++-- app/static/app/js/components/Paginator.jsx | 56 +++++++++++++++----- app/static/app/js/components/ProjectList.jsx | 2 +- app/static/app/js/css/Paginator.scss | 6 +++ 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/app/static/app/js/Dashboard.jsx b/app/static/app/js/Dashboard.jsx index 1b249914..5a876f44 100644 --- a/app/static/app/js/Dashboard.jsx +++ b/app/static/app/js/Dashboard.jsx @@ -41,13 +41,14 @@ class Dashboard extends React.Component { render() { const projectList = ({ location, history }) => { - let q = Utils.queryParams(location), - page = parseInt(q.page !== undefined ? q.page : 1); + let q = Utils.queryParams(location); + if (q.page === undefined) q.page = 1; + else q.page = parseInt(q.page); return { this.projectList = domNode; }} - currentPage={page} + currentPage={q.page} history={history} />; }; diff --git a/app/static/app/js/components/Paginator.jsx b/app/static/app/js/components/Paginator.jsx index 8f69d6cf..ce7a05ea 100644 --- a/app/static/app/js/components/Paginator.jsx +++ b/app/static/app/js/components/Paginator.jsx @@ -1,6 +1,8 @@ import React from 'react'; import '../css/Paginator.scss'; -import { Link } from 'react-router-dom'; +import { Link, withRouter } from 'react-router-dom'; +import SortPanel from './SortPanel'; +import Utils from '../classes/Utils'; import { _ } from '../classes/gettext'; class Paginator extends React.Component { @@ -8,22 +10,52 @@ class Paginator extends React.Component { super(props); this.state = { - showSearch: false + showSearch: false, + sortKey: "-created_at" } + + this.sortItems = [{ + key: "created_at", + label: _("Created on"), + selected: "desc" + },{ + key: "name", + label: _("Name") + },{ + key: "tags", + label: _("Tags") + }]; } toggleSearch = () => { } + sortChanged = key => { + this.setState({sortKey: key}); + setTimeout(() => { + this.props.history.push({search: this.getQueryForPage(this.props.currentPage)}); + }, 0); + } + + getQueryForPage = (num) => { + return Utils.toSearchQuery({ + page: num, + ordering: this.state.sortKey + }); + } + render() { const { itemsPerPage, totalItems, currentPage } = this.props; let paginator = null; let toolbar = (
      -
    • -
    • -
    • +
    • +
    • +
    • + + +
    ); @@ -34,18 +66,18 @@ class Paginator extends React.Component { paginator = (
    • - + «
    • {pages.map(page => { - return (
    • - {toolbar}{paginator}, +
      {toolbar}{paginator}
      , this.props.children, -
      {paginator}
      , +
      {paginator}
      , ]; } } -export default Paginator; +export default withRouter(Paginator); diff --git a/app/static/app/js/components/ProjectList.jsx b/app/static/app/js/components/ProjectList.jsx index 502c4722..5d41578b 100644 --- a/app/static/app/js/components/ProjectList.jsx +++ b/app/static/app/js/components/ProjectList.jsx @@ -113,7 +113,7 @@ class ProjectList extends Paginated { return (
      -
        +
          {this.state.projects.map(p => ( { this["projectListItem_" + p.id] = domNode }} diff --git a/app/static/app/js/css/Paginator.scss b/app/static/app/js/css/Paginator.scss index 1c5bb9c9..655374bc 100644 --- a/app/static/app/js/css/Paginator.scss +++ b/app/static/app/js/css/Paginator.scss @@ -5,4 +5,10 @@ } margin-right: 8px; } + .btn-group.open > .dropdown-menu{ + top: 22px; + a{ + border: none; + } + } } \ No newline at end of file From ac195deee37884907294d524e5382389a39dbc7a Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 8 Mar 2023 14:40:35 -0500 Subject: [PATCH 13/41] Sort panel URL persistency --- app/static/app/js/components/Paginator.jsx | 9 +++++---- app/static/app/js/components/ProjectListItem.jsx | 5 ++--- app/static/app/js/components/SortPanel.jsx | 13 +++++++++++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/static/app/js/components/Paginator.jsx b/app/static/app/js/components/Paginator.jsx index ce7a05ea..11567564 100644 --- a/app/static/app/js/components/Paginator.jsx +++ b/app/static/app/js/components/Paginator.jsx @@ -9,15 +9,16 @@ class Paginator extends React.Component { constructor(props){ super(props); + const q = Utils.queryParams(props.location); + this.state = { showSearch: false, - sortKey: "-created_at" + sortKey: q.ordering } this.sortItems = [{ key: "created_at", - label: _("Created on"), - selected: "desc" + label: _("Created on") },{ key: "name", label: _("Name") @@ -54,7 +55,7 @@ class Paginator extends React.Component {
        • - +
        ); diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx index cb67e2f1..c1431350 100644 --- a/app/static/app/js/components/ProjectListItem.jsx +++ b/app/static/app/js/components/ProjectListItem.jsx @@ -45,8 +45,7 @@ class ProjectListItem extends React.Component { this.sortItems = [{ key: "created_at", - label: _("Created on"), - selected: "desc" + label: _("Created on") },{ key: "name", label: _("Name") @@ -583,7 +582,7 @@ class ProjectListItem extends React.Component { - +
      : ""} diff --git a/app/static/app/js/components/SortPanel.jsx b/app/static/app/js/components/SortPanel.jsx index c7feb7ff..26122079 100644 --- a/app/static/app/js/components/SortPanel.jsx +++ b/app/static/app/js/components/SortPanel.jsx @@ -6,12 +6,14 @@ import { _ } from '../classes/gettext'; class SortPanel extends React.Component { static defaultProps = { items: [], - onChange: () => {} + onChange: () => {}, + selected: null }; static propTypes = { items: PropTypes.arrayOf(PropTypes.object), - onChange: PropTypes.func + onChange: PropTypes.func, + selected: PropTypes.string }; constructor(props){ @@ -20,6 +22,13 @@ class SortPanel extends React.Component { this.state = { items: props.items } + + if (props.selected){ + let normSortKey = props.selected.replace("-", ""); + this.state.items.forEach(s => { + if (s.key === normSortKey) s.selected = props.selected[0] === "-" ? "desc" : "asc"; + }); + } } handleClick = (key, order) => { From b8d7e9f7d23a827c4e9a5f64e18c6c808df608cf Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 9 Mar 2023 13:07:49 -0500 Subject: [PATCH 14/41] PoC search working --- app/static/app/css/theme.scss | 3 + app/static/app/js/Dashboard.jsx | 2 + app/static/app/js/components/EditTaskForm.jsx | 6 +- app/static/app/js/components/Paginator.jsx | 89 +++++++++++++++++-- app/static/app/js/components/ProjectList.jsx | 13 +-- app/static/app/js/components/TaskListItem.jsx | 2 +- app/static/app/js/css/Paginator.scss | 42 +++++++++ package.json | 2 +- 8 files changed, 134 insertions(+), 25 deletions(-) diff --git a/app/static/app/css/theme.scss b/app/static/app/css/theme.scss index d6170d9b..565227a2 100644 --- a/app/static/app/css/theme.scss +++ b/app/static/app/css/theme.scss @@ -61,6 +61,9 @@ body, .pagination li > a{ color: theme("primary"); } +.theme-border-secondary-07{ + border-color: scaleby(theme("secondary"), 0.7) !important; +} .btn-secondary, .btn-secondary:active, .btn-secondary.active, .open>.dropdown-toggle.btn-secondary{ background-color: theme("secondary"); diff --git a/app/static/app/js/Dashboard.jsx b/app/static/app/js/Dashboard.jsx index 5a876f44..30ffeed7 100644 --- a/app/static/app/js/Dashboard.jsx +++ b/app/static/app/js/Dashboard.jsx @@ -44,11 +44,13 @@ class Dashboard extends React.Component { let q = Utils.queryParams(location); if (q.page === undefined) q.page = 1; else q.page = parseInt(q.page); + if (q.search === undefined) q.search = null; return { this.projectList = domNode; }} currentPage={q.page} + currentSearch={q.search} history={history} />; }; diff --git a/app/static/app/js/components/EditTaskForm.jsx b/app/static/app/js/components/EditTaskForm.jsx index 40d053d7..a4330ea1 100644 --- a/app/static/app/js/components/EditTaskForm.jsx +++ b/app/static/app/js/components/EditTaskForm.jsx @@ -46,13 +46,13 @@ class EditTaskForm extends React.Component { processingNodes: [], selectedPreset: null, presets: [], - tags: Utils.clone(props.task.tags), + tags: props.task !== null ? Utils.clone(props.task.tags) : [], editingPreset: false, loadingTaskName: false, - showTagsField: !!props.task.tags.length + showTagsField: props.task !== null ? !!props.task.tags.length : false }; this.handleNameChange = this.handleNameChange.bind(this); @@ -361,7 +361,7 @@ class EditTaskForm extends React.Component { const { name, selectedNode, selectedPreset, tags } = this.state; return { - name: name !== "" ? name : this.namePlaceholder, + name: name !== "" ? name : this.state.namePlaceholder, selectedNode: selectedNode, options: this.getAvailableOptionsOnly(selectedPreset.options, selectedNode.options), tags diff --git a/app/static/app/js/components/Paginator.jsx b/app/static/app/js/components/Paginator.jsx index 11567564..06f6d86f 100644 --- a/app/static/app/js/components/Paginator.jsx +++ b/app/static/app/js/components/Paginator.jsx @@ -12,8 +12,8 @@ class Paginator extends React.Component { const q = Utils.queryParams(props.location); this.state = { - showSearch: false, - sortKey: q.ordering + searchText: "", + sortKey: q.ordering || "-created_at" } this.sortItems = [{ @@ -28,8 +28,57 @@ class Paginator extends React.Component { }]; } - toggleSearch = () => { + stop = e => { + e.stopPropagation(); + } + componentDidMount(){ + this.searchPopup.addEventListener("click", this.stop); + this.searchButton.addEventListener("click", this.toggleSearch); + this.btnSearch.addEventListener("click", this.search); + document.body.addEventListener("click", this.closeSearch); + } + + componentWillUnmount(){ + document.body.removeEventListener("click", this.closeSearch); + this.btnSearch.removeEventListener("click", this.search); + this.searchButton.removeEventListener("click", this.toggleSearch); + this.searchPopup.removeEventListener("click", this.stop); + } + + closeSearch = () => { + this.searchContainer.classList.remove("open"); + } + + toggleSearch = e => { + e.stopPropagation(); + this.searchContainer.classList.toggle("open"); + + setTimeout(() => { + this.searchInput.focus(); + }, 0); + } + + handleSearchChange = e => { + this.setState({searchText: e.target.value}); + } + + handleSearchKeyDown = e => { + if (e.key === "Enter"){ + this.search(); + } + } + + search = () => { + this.props.history.push({search: this.getQueryForPage(1)}); + this.closeSearch(); + } + + clearSearch = () => { + this.setState({searchText: ""}); + setTimeout(() => { + this.search(); + }, 0); } sortChanged = key => { @@ -42,16 +91,36 @@ class Paginator extends React.Component { getQueryForPage = (num) => { return Utils.toSearchQuery({ page: num, - ordering: this.state.sortKey + ordering: this.state.sortKey, + search: this.state.searchText }); } render() { const { itemsPerPage, totalItems, currentPage } = this.props; + const { searchText } = this.state; + let paginator = null; - let toolbar = ( -
        -
      • + let clearSearch = null; + + let toolbar = (
          +
        • { this.searchContainer = domNode; }}> + +
            { this.searchPopup = domNode; }}> +
          • + { this.searchInput = domNode}} + className="form-control search theme-border-secondary-07" + placeholder={_("Search names or #tags")} + value={searchText} + onKeyDown={this.handleSearchKeyDown} + onChange={this.handleSearchChange} /> + +
          • +
          +
        • @@ -60,6 +129,10 @@ class Paginator extends React.Component {
        ); + if (this.props.currentSearch){ + clearSearch = ({_("Search results for:")} {this.props.currentSearch} ×); + } + if (itemsPerPage && itemsPerPage && totalItems > itemsPerPage){ const numPages = Math.ceil(totalItems / itemsPerPage), pages = [...Array(numPages).keys()]; // [0, 1, 2, ...numPages] @@ -87,7 +160,7 @@ class Paginator extends React.Component { } return [ -
        {toolbar}{paginator}
        , +
        {clearSearch}{toolbar}{paginator}
        , this.props.children,
        {paginator}
        , ]; diff --git a/app/static/app/js/components/ProjectList.jsx b/app/static/app/js/components/ProjectList.jsx index 5d41578b..c40eca12 100644 --- a/app/static/app/js/components/ProjectList.jsx +++ b/app/static/app/js/components/ProjectList.jsx @@ -21,8 +21,7 @@ class ProjectList extends Paginated { loading: true, refreshing: false, error: "", - projects: [], - showSearch: false + projects: [] } this.PROJECTS_PER_PAGE = 10; @@ -96,20 +95,10 @@ class ProjectList extends Paginated { this.refresh(); } - toggleSearch = (e) => { - this.setState({showSearch: !this.state.showSearch}); - } - - search = () => { - - } - - render() { if (this.state.loading){ return (
        ); }else{ - let test = (); return (
        diff --git a/app/static/app/js/components/TaskListItem.jsx b/app/static/app/js/components/TaskListItem.jsx index a5383dd1..9c0ed65e 100644 --- a/app/static/app/js/components/TaskListItem.jsx +++ b/app/static/app/js/components/TaskListItem.jsx @@ -404,7 +404,7 @@ class TaskListItem extends React.Component { render() { const task = this.state.task; - const name = task.name !== null ? task.name : _("(unnamed)"); + const name = task.name !== null ? task.name : interpolate(_("Task #%(number)s"), { number: task.id }); const imported = task.import_url !== ""; let status = statusCodes.description(task.status); diff --git a/app/static/app/js/css/Paginator.scss b/app/static/app/js/css/Paginator.scss index 655374bc..7e44109e 100644 --- a/app/static/app/js/css/Paginator.scss +++ b/app/static/app/js/css/Paginator.scss @@ -1,4 +1,8 @@ .paginator{ + display: flex; + justify-content: flex-end; + margin-bottom: 8px; + .toolbar{ i{ opacity: 0.8; @@ -11,4 +15,42 @@ border: none; } } + + .search{ + height: 25px; + margin-left: 7px; + margin-right: 4px; + padding-left: 4px; + padding-right: 4px; + border-width: 1px; + border-radius: 3px; + display: inline-block; + max-width: 210px; + } + + .search-popup{ + min-width: 256px; + + li{ + display: flex; + button{ + width: 27px; + height: 25px; + i{ + position: relative; + top: -4px; + left: -3px; + } + } + } + } + + .clear-search{ + margin-top: 1px; + font-weight: bold; + margin-right: 8px; + .query{ + font-weight: normal; + } + } } \ No newline at end of file diff --git a/package.json b/package.json index 8dded749..39c3efe0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "1.9.19", + "version": "2.0.0", "description": "User-friendly, extendable application and API for processing aerial imagery.", "main": "index.js", "scripts": { From 8a51317774469deeceac4787ee4e22fb4d3afe1d Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 13 Mar 2023 11:33:33 -0400 Subject: [PATCH 15/41] Search working --- app/api/projects.py | 27 +++++++++++++++++++--- app/static/app/js/Dashboard.jsx | 1 - app/static/app/js/components/Paginator.jsx | 13 +++++++---- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/app/api/projects.py b/app/api/projects.py index 02ac874e..848aed37 100644 --- a/app/api/projects.py +++ b/app/api/projects.py @@ -1,3 +1,4 @@ +import re from guardian.shortcuts import get_perms, get_users_with_perms, assign_perm, remove_perm from rest_framework import serializers, viewsets from rest_framework.decorators import action @@ -6,6 +7,8 @@ from rest_framework import status from django_filters import rest_framework as filters from django.db import transaction from django.contrib.auth.models import User +from django.contrib.postgres.search import SearchQuery, SearchVector +from django.contrib.postgres.aggregates import StringAgg from django.db.models import Q from app import models @@ -41,9 +44,27 @@ class ProjectSerializer(serializers.ModelSerializer): class ProjectFilter(filters.FilterSet): search = filters.CharFilter(method='filter_search') - def filter_search(self, queryset, name, value): - print(name, value) - return queryset.filter(Q(name__icontains=value) | Q(task__name__icontains=value)).distinct() + def filter_search(self, qs, name, value): + value = value.replace(":", "#") + tag_pattern = re.compile("#[^\s]+") + tags = re.findall(tag_pattern, value) + names = re.sub("\s+", " ", re.sub(tag_pattern, "", value)).strip() + + if len(names) > 0: + project_name_vec = SearchVector("name") + task_name_vec = SearchVector(StringAgg("task__name", delimiter=' ')) + name_query = SearchQuery(names, search_type="plain") + qs = qs.annotate(n_search=project_name_vec + task_name_vec).filter(n_search=name_query) + + if len(tags) > 0: + project_tags_vec = SearchVector("tags") + task_tags_vec = SearchVector(StringAgg("task__tags", delimiter=' ')) + tags_query = SearchQuery(tags[0]) + for t in tags[1:]: + tags_query = tags_query & SearchQuery(t) + qs = qs.annotate(t_search=project_tags_vec + task_tags_vec).filter(t_search=tags_query) + + return qs.distinct() class Meta: model = models.Project diff --git a/app/static/app/js/Dashboard.jsx b/app/static/app/js/Dashboard.jsx index 30ffeed7..3c154444 100644 --- a/app/static/app/js/Dashboard.jsx +++ b/app/static/app/js/Dashboard.jsx @@ -44,7 +44,6 @@ class Dashboard extends React.Component { let q = Utils.queryParams(location); if (q.page === undefined) q.page = 1; else q.page = parseInt(q.page); - if (q.search === undefined) q.search = null; return { + return window.decodeURI(search.replace(/:/g, "#")); +}; + class Paginator extends React.Component { constructor(props){ super(props); const q = Utils.queryParams(props.location); - + this.state = { - searchText: "", + searchText: decodeSearch(q.search || ""), sortKey: q.ordering || "-created_at" } @@ -92,7 +96,7 @@ class Paginator extends React.Component { return Utils.toSearchQuery({ page: num, ordering: this.state.sortKey, - search: this.state.searchText + search: this.state.searchText.replace(/#/g, ":") }); } @@ -130,7 +134,8 @@ class Paginator extends React.Component { ); if (this.props.currentSearch){ - clearSearch = ({_("Search results for:")} {this.props.currentSearch} ×); + let currentSearch = decodeSearch(this.props.currentSearch); + clearSearch = ({_("Search results for:")} {currentSearch} ×); } if (itemsPerPage && itemsPerPage && totalItems > itemsPerPage){ From c852f72b203161c2dc3d64ea5388cecb0c6cd5d9 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 13 Mar 2023 12:28:18 -0400 Subject: [PATCH 16/41] Project tags only search --- app/api/projects.py | 24 +++++++++++++++---- app/static/app/js/components/Paginator.jsx | 17 ++++++++++++- .../app/js/components/ProjectListItem.jsx | 9 ++++++- app/static/app/js/css/ProjectListItem.scss | 3 +++ 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/app/api/projects.py b/app/api/projects.py index 848aed37..a3b30591 100644 --- a/app/api/projects.py +++ b/app/api/projects.py @@ -47,7 +47,14 @@ class ProjectFilter(filters.FilterSet): def filter_search(self, qs, name, value): value = value.replace(":", "#") tag_pattern = re.compile("#[^\s]+") - tags = re.findall(tag_pattern, value) + tags = set(re.findall(tag_pattern, value)) + + project_tags = set([t for t in tags if t.startswith("##")]) + deep_tags = tags - project_tags + + project_tags = [t.replace("##", "") for t in project_tags] + deep_tags = [t.replace("#", "") for t in deep_tags] + names = re.sub("\s+", " ", re.sub(tag_pattern, "", value)).strip() if len(names) > 0: @@ -56,13 +63,20 @@ class ProjectFilter(filters.FilterSet): name_query = SearchQuery(names, search_type="plain") qs = qs.annotate(n_search=project_name_vec + task_name_vec).filter(n_search=name_query) - if len(tags) > 0: + if len(deep_tags) > 0: project_tags_vec = SearchVector("tags") task_tags_vec = SearchVector(StringAgg("task__tags", delimiter=' ')) - tags_query = SearchQuery(tags[0]) - for t in tags[1:]: + tags_query = SearchQuery(deep_tags[0]) + for t in deep_tags[1:]: tags_query = tags_query & SearchQuery(t) - qs = qs.annotate(t_search=project_tags_vec + task_tags_vec).filter(t_search=tags_query) + qs = qs.annotate(dt_search=project_tags_vec + task_tags_vec).filter(dt_search=tags_query) + + if len(project_tags) > 0: + project_tags_vec = SearchVector("tags") + tags_query = SearchQuery(project_tags[0]) + for t in project_tags[1:]: + tags_query = tags_query & SearchQuery(t) + qs = qs.annotate(pt_search=project_tags_vec).filter(pt_search=tags_query) return qs.distinct() diff --git a/app/static/app/js/components/Paginator.jsx b/app/static/app/js/components/Paginator.jsx index 1c99172e..61a2556e 100644 --- a/app/static/app/js/components/Paginator.jsx +++ b/app/static/app/js/components/Paginator.jsx @@ -41,9 +41,11 @@ class Paginator extends React.Component { this.searchButton.addEventListener("click", this.toggleSearch); this.btnSearch.addEventListener("click", this.search); document.body.addEventListener("click", this.closeSearch); + document.addEventListener("onProjectListTagClicked", this.addTagAndSearch); } componentWillUnmount(){ + document.removeEventListener("onProjectListTagClicked", this.addTagAndSearch); document.body.removeEventListener("click", this.closeSearch); this.btnSearch.removeEventListener("click", this.search); this.searchButton.removeEventListener("click", this.toggleSearch); @@ -100,6 +102,20 @@ class Paginator extends React.Component { }); } + addTagAndSearch = e => { + const tag = e.detail; + if (tag === undefined) return; + + let { searchText } = this.state; + if (searchText === "") searchText += "##" + tag; + else searchText += " ##" + tag; + + this.setState({searchText}); + setTimeout(() => { + this.search(); + }, 0); + } + render() { const { itemsPerPage, totalItems, currentPage } = this.props; const { searchText } = this.state; @@ -125,7 +141,6 @@ class Paginator extends React.Component {
    • -
    • diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx index c1431350..2916b5e4 100644 --- a/app/static/app/js/components/ProjectListItem.jsx +++ b/app/static/app/js/components/ProjectListItem.jsx @@ -491,6 +491,13 @@ class ProjectListItem extends React.Component { } } + handleTagClick = tag => { + return e => { + const evt = new CustomEvent("onProjectListTagClicked", { detail: tag }); + document.dispatchEvent(evt); + } + } + render() { const { refreshing, data } = this.state; const numTasks = data.tasks.length; @@ -555,7 +562,7 @@ class ProjectListItem extends React.Component {
      {data.name} {userTags.length > 0 ? - userTags.map((t, i) =>
      {t}
      ) + userTags.map((t, i) =>
      {t}
      ) : ""}
      diff --git a/app/static/app/js/css/ProjectListItem.scss b/app/static/app/js/css/ProjectListItem.scss index 4ae129cc..77b7e0bf 100644 --- a/app/static/app/js/css/ProjectListItem.scss +++ b/app/static/app/js/css/ProjectListItem.scss @@ -119,5 +119,8 @@ font-size: 90%; position: relative; top: -1px; + &:hover{ + cursor: pointer; + } } } From c90b575850c936a52e5e18dc16d218d4df60ec76 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 13 Mar 2023 15:15:02 -0400 Subject: [PATCH 17/41] Fixes, some refactoring --- app/api/projects.py | 8 +-- app/static/app/js/components/Paginator.jsx | 71 ++++++++----------- app/static/app/js/components/ProjectList.jsx | 18 ++++- .../app/js/components/ProjectListItem.jsx | 25 +++++-- app/static/app/js/components/TaskList.jsx | 37 +++++++++- app/static/app/js/components/TaskListItem.jsx | 4 +- app/static/app/js/css/Paginator.scss | 3 + app/static/app/js/vendor/bootstrap.js | 11 ++- app/static/app/js/vendor/bootstrap.min.js | 6 +- 9 files changed, 123 insertions(+), 60 deletions(-) diff --git a/app/api/projects.py b/app/api/projects.py index a3b30591..00212c39 100644 --- a/app/api/projects.py +++ b/app/api/projects.py @@ -49,11 +49,11 @@ class ProjectFilter(filters.FilterSet): tag_pattern = re.compile("#[^\s]+") tags = set(re.findall(tag_pattern, value)) - project_tags = set([t for t in tags if t.startswith("##")]) - deep_tags = tags - project_tags + deep_tags = set([t for t in tags if t.startswith("##")]) + project_tags = tags - deep_tags - project_tags = [t.replace("##", "") for t in project_tags] - deep_tags = [t.replace("#", "") for t in deep_tags] + deep_tags = [t.replace("##", "") for t in deep_tags] + project_tags = [t.replace("#", "") for t in project_tags] names = re.sub("\s+", " ", re.sub(tag_pattern, "", value)).strip() diff --git a/app/static/app/js/components/Paginator.jsx b/app/static/app/js/components/Paginator.jsx index 61a2556e..9ab567b5 100644 --- a/app/static/app/js/components/Paginator.jsx +++ b/app/static/app/js/components/Paginator.jsx @@ -32,24 +32,12 @@ class Paginator extends React.Component { }]; } - stop = e => { - e.stopPropagation(); - } - componentDidMount(){ - this.searchPopup.addEventListener("click", this.stop); - this.searchButton.addEventListener("click", this.toggleSearch); - this.btnSearch.addEventListener("click", this.search); - document.body.addEventListener("click", this.closeSearch); document.addEventListener("onProjectListTagClicked", this.addTagAndSearch); } componentWillUnmount(){ document.removeEventListener("onProjectListTagClicked", this.addTagAndSearch); - document.body.removeEventListener("click", this.closeSearch); - this.btnSearch.removeEventListener("click", this.search); - this.searchButton.removeEventListener("click", this.toggleSearch); - this.searchPopup.removeEventListener("click", this.stop); } closeSearch = () => { @@ -58,11 +46,9 @@ class Paginator extends React.Component { toggleSearch = e => { e.stopPropagation(); - this.searchContainer.classList.toggle("open"); - setTimeout(() => { this.searchInput.focus(); - }, 0); + }, 50); } handleSearchChange = e => { @@ -107,8 +93,8 @@ class Paginator extends React.Component { if (tag === undefined) return; let { searchText } = this.state; - if (searchText === "") searchText += "##" + tag; - else searchText += " ##" + tag; + if (searchText === "") searchText += "#" + tag; + else searchText += " #" + tag; this.setState({searchText}); setTimeout(() => { @@ -122,31 +108,32 @@ class Paginator extends React.Component { let paginator = null; let clearSearch = null; - - let toolbar = (
        -
      • { this.searchContainer = domNode; }}> - -
          { this.searchPopup = domNode; }}> -
        • - { this.searchInput = domNode}} - className="form-control search theme-border-secondary-07" - placeholder={_("Search names or #tags")} - value={searchText} - onKeyDown={this.handleSearchKeyDown} - onChange={this.handleSearchChange} /> - -
        • -
        -
      • -
      • - - -
      • -
      - ); + let toolbar = (
        +
      • { this.searchContainer = domNode; }}> + +
          +
        • + { this.searchInput = domNode}} + className="form-control search theme-border-secondary-07" + placeholder={_("Search names or #tags")} + value={searchText} + onKeyDown={this.handleSearchKeyDown} + onChange={this.handleSearchChange} /> + +
        • +
        +
      • +
      • + + +
      • +
      ); if (this.props.currentSearch){ let currentSearch = decodeSearch(this.props.currentSearch); diff --git a/app/static/app/js/components/ProjectList.jsx b/app/static/app/js/components/ProjectList.jsx index c40eca12..c52d8547 100644 --- a/app/static/app/js/components/ProjectList.jsx +++ b/app/static/app/js/components/ProjectList.jsx @@ -8,6 +8,7 @@ import Paginator from './Paginator'; import ErrorMessage from './ErrorMessage'; import { _, interpolate } from '../classes/gettext'; import PropTypes from 'prop-types'; +import Utils from '../classes/Utils'; class ProjectList extends Paginated { static propTypes = { @@ -33,8 +34,23 @@ class ProjectList extends Paginated { this.refresh(); } + getParametersHash(source){ + if (!source) return ""; + if (source.indexOf("?") === -1) return ""; + + let search = source.substr(source.indexOf("?")); + let q = Utils.queryParams({search}); + + // All parameters that can change via history.push without + // triggering a reload of the project list should go here + delete q.project_task_open; + delete q.project_task_expanded; + + return JSON.stringify(q); + } + componentDidUpdate(prevProps){ - if (prevProps.source !== this.props.source){ + if (this.getParametersHash(prevProps.source) !== this.getParametersHash(this.props.source)){ this.refresh(); } } diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx index 2916b5e4..f495376a 100644 --- a/app/static/app/js/components/ProjectListItem.jsx +++ b/app/static/app/js/components/ProjectListItem.jsx @@ -40,7 +40,8 @@ class ProjectListItem extends React.Component { refreshing: false, importing: false, buttons: [], - sortKey: "-created_at" + sortKey: "-created_at", + filterTags: [] }; this.sortItems = [{ @@ -498,8 +499,12 @@ class ProjectListItem extends React.Component { } } + tagsChanged = (filterTags) => { + this.setState({filterTags}); + } + render() { - const { refreshing, data } = this.state; + const { refreshing, data, filterTags } = this.state; const numTasks = data.tasks.length; const canEdit = this.hasPermission("change"); const userTags = Tags.userTags(data.tags); @@ -580,10 +585,17 @@ class ProjectListItem extends React.Component { {this.state.showTaskList && numTasks > 1 ?
      - - - {_("Filter")} - + {filterTags.length > 0 ? +
      + + +
        + {filterTags.map(t =>
      • {t}
      • )} +
      +
      + : ""}
    • Date: Wed, 22 Mar 2023 11:43:14 -0400 Subject: [PATCH 29/41] Assign duplicate projects to user that duplicated them --- app/api/projects.py | 2 +- app/models/project.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/api/projects.py b/app/api/projects.py index d5ea990c..308198da 100644 --- a/app/api/projects.py +++ b/app/api/projects.py @@ -118,7 +118,7 @@ class ProjectViewSet(viewsets.ModelViewSet): """ project = get_and_check_project(request, pk, ('change_project', )) - new_project = project.duplicate() + new_project = project.duplicate(new_owner=request.user) if new_project: return Response({'success': True, 'project': ProjectSerializer(new_project).data}, status=status.HTTP_200_OK) else: diff --git a/app/models/project.py b/app/models/project.py index 1d316b45..dc532da2 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -54,13 +54,15 @@ class Project(models.Model): ).filter(Q(orthophoto_extent__isnull=False) | Q(dsm_extent__isnull=False) | Q(dtm_extent__isnull=False)) .only('id', 'project_id')] - def duplicate(self): + def duplicate(self, new_owner=None): try: with transaction.atomic(): project = Project.objects.get(pk=self.pk) project.pk = None project.name = gettext('Copy of %(task)s') % {'task': self.name} project.created_at = timezone.now() + if new_owner is not None: + project.owner = new_owner project.save() project.refresh_from_db() From f0b15c2b2f14ebd750f4a59a144240b194fb16fb Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 22 Mar 2023 11:45:56 -0400 Subject: [PATCH 30/41] Update translations --- .../app/js/translations/odm_autogenerated.js | 172 +++++++++--------- locale | 2 +- 2 files changed, 87 insertions(+), 87 deletions(-) diff --git a/app/static/app/js/translations/odm_autogenerated.js b/app/static/app/js/translations/odm_autogenerated.js index b51ba74c..9b022dfb 100644 --- a/app/static/app/js/translations/odm_autogenerated.js +++ b/app/static/app/js/translations/odm_autogenerated.js @@ -1,93 +1,93 @@ // Auto-generated with extract_odm_strings.py, do not edit! +_("Skip normalization of colors across all images. Useful when processing radiometric data. Default: %(default)s"); +_("Displays version number and exits. "); +_("Automatically set a boundary using camera shot locations to limit the area of the reconstruction. This can help remove far away background artifacts (sky, background landscapes, etc.). See also --boundary. Default: %(default)s"); +_("The maximum vertex count of the output mesh. Default: %(default)s"); +_("URL to a ClusterODM instance for distributing a split-merge workflow on multiple nodes in parallel. Default: %(default)s"); +_("Radius of the overlap between submodels. After grouping images into clusters, images that are closer than this radius to a cluster are added to the cluster. This is done to ensure that neighboring submodels overlap. Default: %(default)s"); +_("Save the georeferenced point cloud in Cloud Optimized Point Cloud (COPC) format. Default: %(default)s"); +_("Choose the algorithm for extracting keypoints and computing descriptors. Can be one of: %(choices)s. Default: %(default)s"); +_("Matcher algorithm, Fast Library for Approximate Nearest Neighbors or Bag of Words. FLANN is slower, but more stable. BOW is faster, but can sometimes miss valid matches. BRUTEFORCE is very slow but robust.Can be one of: %(choices)s. Default: %(default)s"); +_("Set a value in meters for the GPS Dilution of Precision (DOP) information for all images. If your images are tagged with high precision GPS information (RTK), this value will be automatically set accordingly. You can use this option to manually set it in case the reconstruction fails. Lowering this option can sometimes help control bowling-effects over large areas. Default: %(default)s"); +_("Maximum number of frames to extract from video files for processing. Set to 0 for no limit. Default: %(default)s"); +_("Generate single file Binary glTF (GLB) textured models. Default: %(default)s"); +_("Simple Morphological Filter elevation scalar parameter. Default: %(default)s"); +_("Permanently delete all previous results and rerun the processing pipeline."); +_("Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud and remove duplicate points. Set to 0 to disable sampling. Default: %(default)s"); +_("Octree depth used in the mesh reconstruction, increase to get more vertices, recommended values are 8-12. Default: %(default)s"); +_("Export the georeferenced point cloud in CSV format. Default: %(default)s"); +_("Turn off camera parameter optimization during bundle adjustment. This can be sometimes useful for improving results that exhibit doming/bowling or when images are taken with a rolling shutter camera. Default: %(default)s"); +_("Skip generation of a full 3D model. This can save time if you only need 2D results such as orthophotos and DEMs. Default: %(default)s"); +_("Generate static tiles for orthophotos and DEMs that are suitable for viewers like Leaflet or OpenLayers. Default: %(default)s"); +_("Copy output results to this folder after processing."); +_("Ignore Ground Sampling Distance (GSD). GSD caps the maximum resolution of image outputs and resizes images when necessary, resulting in faster processing and lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality. Default: %(default)s"); +_("Use this tag if you have a GCP File but want to use the EXIF information for georeferencing instead. Default: %(default)s"); +_("Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive morphological filter. Check the --dem* parameters for finer tuning. Default: %(default)s"); +_("Skip the blending of colors near seams. Default: %(default)s"); +_("Perform image matching with the nearest images based on GPS exif data. Set to 0 to match by triangulation. Default: %(default)s"); +_("End processing at this stage. Can be one of: %(choices)s. Default: %(default)s"); +_("Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s"); +_("Skip generation of the orthophoto. This can save time if you only need 3D results or DEMs. Default: %(default)s"); +_("Rerun this stage only and stop. Can be one of: %(choices)s. Default: %(default)s"); +_("Skip generation of PDF report. This can save time if you don't need a report. Default: %(default)s"); +_("Choose the structure from motion algorithm. For aerial datasets, if camera GPS positions and angles are available, triangulation can generate better results. For planar scenes captured at fixed altitude with nadir-only images, planar can be much faster. Can be one of: %(choices)s. Default: %(default)s"); +_("Delete heavy intermediate files to optimize disk space usage. This affects the ability to restart the pipeline from an intermediate stage, but allows datasets to be processed on machines that don't have sufficient disk space available. Default: %(default)s"); +_("When processing multispectral datasets, ODM will automatically align the images for each band. If the images have been postprocessed and are already aligned, use this option. Default: %(default)s"); +_("Run local bundle adjustment for every image added to the reconstruction and a global adjustment every 100 images. Speeds up reconstruction for very large datasets. Default: %(default)s"); +_("Automatically compute image masks using AI to remove the background. Experimental. Default: %(default)s"); +_("Use this tag to build a DTM (Digital Terrain Model, ground only) using a simple morphological filter. Check the --dem* and --smrf* parameters for finer tuning. Default: %(default)s"); +_("Filters the point cloud by removing points that deviate more than N standard deviations from the local mean. Set to 0 to disable filtering. Default: %(default)s"); +_("GeoJSON polygon limiting the area of the reconstruction. Can be specified either as path to a GeoJSON file or as a JSON string representing the contents of a GeoJSON file. Default: %(default)s"); +_("Automatically crop image outputs by creating a smooth buffer around the dataset boundaries, shrunk by N meters. Use 0 to disable cropping. Default: %(default)s"); +_("show this help message and exit"); +_("Set point cloud quality. Higher quality generates better, denser point clouds, but requires more memory and takes longer. Each step up in quality increases processing time roughly by a factor of 4x.Can be one of: %(choices)s. Default: %(default)s"); +_("Choose what to merge in the merge step in a split dataset. By default all available outputs are merged. Options: %(choices)s. Default: %(default)s"); _("Keep faces in the mesh that are not seen in any camera. Default: %(default)s"); _("Set this parameter if you want a striped GeoTIFF. Default: %(default)s"); -_("End processing at this stage. Can be one of: %(choices)s. Default: %(default)s"); -_("The maximum vertex count of the output mesh. Default: %(default)s"); -_("Create Cloud-Optimized GeoTIFFs instead of normal GeoTIFFs. Default: %(default)s"); -_("Perform ground rectification on the point cloud. This means that wrongly classified ground points will be re-classified and gaps will be filled. Useful for generating DTMs. Default: %(default)s"); -_("The maximum output resolution of extracted video frames in pixels. Default: %(default)s"); -_("Choose the algorithm for extracting keypoints and computing descriptors. Can be one of: %(choices)s. Default: %(default)s"); -_("Set the compression to use for orthophotos. Can be one of: %(choices)s. Default: %(default)s"); -_("Generates a polygon around the cropping area that cuts the orthophoto around the edges of features. This polygon can be useful for stitching seamless mosaics with multiple overlapping orthophotos. Default: %(default)s"); -_("Use a full 3D mesh to compute the orthophoto instead of a 2.5D mesh. This option is a bit faster and provides similar results in planar areas. Default: %(default)s"); -_("Path to the file containing the ground control points used for georeferencing. The file needs to use the following format: EPSG: or <+proj definition>geo_x geo_y geo_z im_x im_y image_name [gcp_name] [extra1] [extra2]Default: %(default)s"); -_("Generate OGC 3D Tiles outputs. Default: %(default)s"); -_("Simple Morphological Filter elevation scalar parameter. Default: %(default)s"); -_("Copy output results to this folder after processing."); -_("Set the radiometric calibration to perform on images. When processing multispectral and thermal images you should set this option to obtain reflectance/temperature values (otherwise you will get digital number values). [camera] applies black level, vignetting, row gradient gain/exposure compensation (if appropriate EXIF tags are found) and computes absolute temperature values. [camera+sun] is experimental, applies all the corrections of [camera], plus compensates for spectral radiance registered via a downwelling light sensor (DLS) taking in consideration the angle of the sun. Can be one of: %(choices)s. Default: %(default)s"); -_("When processing multispectral datasets, ODM will automatically align the images for each band. If the images have been postprocessed and are already aligned, use this option. Default: %(default)s"); -_("Generate static tiles for orthophotos and DEMs that are suitable for viewers like Leaflet or OpenLayers. Default: %(default)s"); -_("Do not use GPU acceleration, even if it's available. Default: %(default)s"); -_("GeoJSON polygon limiting the area of the reconstruction. Can be specified either as path to a GeoJSON file or as a JSON string representing the contents of a GeoJSON file. Default: %(default)s"); -_("Path to the project folder. Your project folder should contain subfolders for each dataset. Each dataset should have an \"images\" folder."); -_("Perform image matching with the nearest images based on GPS exif data. Set to 0 to match by triangulation. Default: %(default)s"); -_("Set this parameter if you want to generate a PNG rendering of the orthophoto. Default: %(default)s"); -_("Export the georeferenced point cloud in CSV format. Default: %(default)s"); -_("Rerun this stage only and stop. Can be one of: %(choices)s. Default: %(default)s"); -_("Run local bundle adjustment for every image added to the reconstruction and a global adjustment every 100 images. Speeds up reconstruction for very large datasets. Default: %(default)s"); -_("Skip normalization of colors across all images. Useful when processing radiometric data. Default: %(default)s"); -_("Name of dataset (i.e subfolder name within project folder). Default: %(default)s"); -_("DSM/DTM resolution in cm / pixel. Note that this value is capped to 2x the ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also. Default: %(default)s"); -_("Turn on rolling shutter correction. If the camera has a rolling shutter and the images were taken in motion, you can turn on this option to improve the accuracy of the results. See also --rolling-shutter-readout. Default: %(default)s"); -_("Simple Morphological Filter elevation threshold parameter (meters). Default: %(default)s"); -_("Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. Starting with a radius equal to the output resolution, N different DEMs are generated with progressively bigger radius using the inverse distance weighted (IDW) algorithm and merged together. Remaining gaps are then merged using nearest neighbor interpolation. Default: %(default)s"); -_("Matcher algorithm, Fast Library for Approximate Nearest Neighbors or Bag of Words. FLANN is slower, but more stable. BOW is faster, but can sometimes miss valid matches. BRUTEFORCE is very slow but robust.Can be one of: %(choices)s. Default: %(default)s"); -_("Simple Morphological Filter window radius parameter (meters). Default: %(default)s"); -_("Export the georeferenced point cloud in LAS format. Default: %(default)s"); -_("Filters the point cloud by removing points that deviate more than N standard deviations from the local mean. Set to 0 to disable filtering. Default: %(default)s"); -_("Skip alignment of submodels in split-merge. Useful if GPS is good enough on very large datasets. Default: %(default)s"); -_("Set feature extraction quality. Higher quality generates better features, but requires more memory and takes longer. Can be one of: %(choices)s. Default: %(default)s"); -_("Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud and remove duplicate points. Set to 0 to disable sampling. Default: %(default)s"); -_("Automatically crop image outputs by creating a smooth buffer around the dataset boundaries, shrunk by N meters. Use 0 to disable cropping. Default: %(default)s"); -_("Displays version number and exits. "); -_("Choose what to merge in the merge step in a split dataset. By default all available outputs are merged. Options: %(choices)s. Default: %(default)s"); -_("Specify the distance between camera shot locations and the outer edge of the boundary when computing the boundary with --auto-boundary. Set to 0 to automatically choose a value. Default: %(default)s"); -_("Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive morphological filter. Check the --dem* parameters for finer tuning. Default: %(default)s"); -_("Reduce the memory usage needed for depthmap fusion by splitting large scenes into tiles. Turn this on if your machine doesn't have much RAM and/or you've set --pc-quality to high or ultra. Experimental. Default: %(default)s"); -_("Path to a GeoTIFF DEM or a LAS/LAZ point cloud that the reconstruction outputs should be automatically aligned to. Experimental. Default: %(default)s"); -_("Simple Morphological Filter slope parameter (rise over run). Default: %(default)s"); -_("The maximum number of processes to use in various processes. Peak memory requirement is ~1GB per thread and 2 megapixel image resolution. Default: %(default)s"); -_("Skip generation of PDF report. This can save time if you don't need a report. Default: %(default)s"); -_("Permanently delete all previous results and rerun the processing pipeline."); -_("Set a camera projection type. Manually setting a value can help improve geometric undistortion. By default the application tries to determine a lens type from the images metadata. Can be one of: %(choices)s. Default: %(default)s"); -_("URL to a ClusterODM instance for distributing a split-merge workflow on multiple nodes in parallel. Default: %(default)s"); -_("Computes an euclidean raster map for each DEM. The map reports the distance from each cell to the nearest NODATA value (before any hole filling takes place). This can be useful to isolate the areas that have been filled. Default: %(default)s"); -_("Rerun processing from this stage. Can be one of: %(choices)s. Default: %(default)s"); -_("Average number of images per submodel. When splitting a large dataset into smaller submodels, images are grouped into clusters. This value regulates the number of images that each cluster should have on average. Default: %(default)s"); -_("Automatically compute image masks using AI to remove the background. Experimental. Default: %(default)s"); -_("Geometric estimates improve the accuracy of the point cloud by computing geometrically consistent depthmaps but may not be usable in larger datasets. This flag disables geometric estimates. Default: %(default)s"); -_("Decimate the points before generating the DEM. 1 is no decimation (full quality). 100 decimates ~99%% of the points. Useful for speeding up generation of DEM results in very large datasets. Default: %(default)s"); -_("Path to the image geolocation file containing the camera center coordinates used for georeferencing. If you don't have values for yaw/pitch/roll you can set them to 0. The file needs to use the following format: EPSG: or <+proj definition>image_name geo_x geo_y geo_z [yaw (degrees)] [pitch (degrees)] [roll (degrees)] [horz accuracy (meters)] [vert accuracy (meters)]Default: %(default)s"); -_("Skip generation of a full 3D model. This can save time if you only need 2D results such as orthophotos and DEMs. Default: %(default)s"); -_("Generate single file Binary glTF (GLB) textured models. Default: %(default)s"); -_("Set this parameter if you want to generate a Google Earth (KMZ) rendering of the orthophoto. Default: %(default)s"); -_("Use images' GPS exif data for reconstruction, even if there are GCPs present.This flag is useful if you have high precision GPS measurements. If there are no GCPs, this flag does nothing. Default: %(default)s"); -_("Maximum number of frames to extract from video files for processing. Set to 0 for no limit. Default: %(default)s"); -_("Use the camera parameters computed from another dataset instead of calculating them. Can be specified either as path to a cameras.json file or as a JSON string representing the contents of a cameras.json file. Default: %(default)s"); -_("Skip generation of the orthophoto. This can save time if you only need 3D results or DEMs. Default: %(default)s"); -_("Set a value in meters for the GPS Dilution of Precision (DOP) information for all images. If your images are tagged with high precision GPS information (RTK), this value will be automatically set accordingly. You can use this option to manually set it in case the reconstruction fails. Lowering this option can sometimes help control bowling-effects over large areas. Default: %(default)s"); -_("show this help message and exit"); -_("Skips dense reconstruction and 3D model generation. It generates an orthophoto directly from the sparse reconstruction. If you just need an orthophoto and do not need a full 3D model, turn on this option. Default: %(default)s"); -_("Delete heavy intermediate files to optimize disk space usage. This affects the ability to restart the pipeline from an intermediate stage, but allows datasets to be processed on machines that don't have sufficient disk space available. Default: %(default)s"); -_("Set point cloud quality. Higher quality generates better, denser point clouds, but requires more memory and takes longer. Each step up in quality increases processing time roughly by a factor of 4x.Can be one of: %(choices)s. Default: %(default)s"); -_("Automatically set a boundary using camera shot locations to limit the area of the reconstruction. This can help remove far away background artifacts (sky, background landscapes, etc.). See also --boundary. Default: %(default)s"); -_("Automatically compute image masks using AI to remove the sky. Experimental. Default: %(default)s"); -_("Octree depth used in the mesh reconstruction, increase to get more vertices, recommended values are 8-12. Default: %(default)s"); -_("Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s"); -_("Override the rolling shutter readout time for your camera sensor (in milliseconds), instead of using the rolling shutter readout database. Note that not all cameras are present in the database. Set to 0 to use the database value. Default: %(default)s"); -_("Save the georeferenced point cloud in Cloud Optimized Point Cloud (COPC) format. Default: %(default)s"); -_("Minimum number of features to extract per image. More features can be useful for finding more matches between images, potentially allowing the reconstruction of areas with little overlap or insufficient features. More features also slow down processing. Default: %(default)s"); -_("Classify the point cloud outputs using a Simple Morphological Filter. You can control the behavior of this option by tweaking the --dem-* parameters. Default: %(default)s"); -_("Choose the structure from motion algorithm. For aerial datasets, if camera GPS positions and angles are available, triangulation can generate better results. For planar scenes captured at fixed altitude with nadir-only images, planar can be much faster. Can be one of: %(choices)s. Default: %(default)s"); -_("Turn off camera parameter optimization during bundle adjustment. This can be sometimes useful for improving results that exhibit doming/bowling or when images are taken with a rolling shutter camera. Default: %(default)s"); -_("Skip the blending of colors near seams. Default: %(default)s"); -_("Use this tag if you have a GCP File but want to use the EXIF information for georeferencing instead. Default: %(default)s"); -_("Build orthophoto overviews for faster display in programs such as QGIS. Default: %(default)s"); _("Path to the image groups file that controls how images should be split into groups. The file needs to use the following format: image_name group_nameDefault: %(default)s"); +_("Automatically compute image masks using AI to remove the sky. Experimental. Default: %(default)s"); +_("Specify the distance between camera shot locations and the outer edge of the boundary when computing the boundary with --auto-boundary. Set to 0 to automatically choose a value. Default: %(default)s"); +_("Reduce the memory usage needed for depthmap fusion by splitting large scenes into tiles. Turn this on if your machine doesn't have much RAM and/or you've set --pc-quality to high or ultra. Experimental. Default: %(default)s"); +_("Use the camera parameters computed from another dataset instead of calculating them. Can be specified either as path to a cameras.json file or as a JSON string representing the contents of a cameras.json file. Default: %(default)s"); _("Generate OBJs that have a single material and a single texture file instead of multiple ones. Default: %(default)s"); -_("When processing multispectral datasets, you can specify the name of the primary band that will be used for reconstruction. It's recommended to choose a band which has sharp details and is in focus. Default: %(default)s"); +_("Use images' GPS exif data for reconstruction, even if there are GCPs present.This flag is useful if you have high precision GPS measurements. If there are no GCPs, this flag does nothing. Default: %(default)s"); +_("Simple Morphological Filter slope parameter (rise over run). Default: %(default)s"); +_("Build orthophoto overviews for faster display in programs such as QGIS. Default: %(default)s"); +_("Generates a polygon around the cropping area that cuts the orthophoto around the edges of features. This polygon can be useful for stitching seamless mosaics with multiple overlapping orthophotos. Default: %(default)s"); +_("Turn on rolling shutter correction. If the camera has a rolling shutter and the images were taken in motion, you can turn on this option to improve the accuracy of the results. See also --rolling-shutter-readout. Default: %(default)s"); +_("Skip alignment of submodels in split-merge. Useful if GPS is good enough on very large datasets. Default: %(default)s"); +_("Geometric estimates improve the accuracy of the point cloud by computing geometrically consistent depthmaps but may not be usable in larger datasets. This flag disables geometric estimates. Default: %(default)s"); +_("The maximum number of processes to use in various processes. Peak memory requirement is ~1GB per thread and 2 megapixel image resolution. Default: %(default)s"); +_("Path to a GeoTIFF DEM or a LAS/LAZ point cloud that the reconstruction outputs should be automatically aligned to. Experimental. Default: %(default)s"); +_("DSM/DTM resolution in cm / pixel. Note that this value is capped to 2x the ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also. Default: %(default)s"); +_("Minimum number of features to extract per image. More features can be useful for finding more matches between images, potentially allowing the reconstruction of areas with little overlap or insufficient features. More features also slow down processing. Default: %(default)s"); +_("Rerun processing from this stage. Can be one of: %(choices)s. Default: %(default)s"); +_("Perform ground rectification on the point cloud. This means that wrongly classified ground points will be re-classified and gaps will be filled. Useful for generating DTMs. Default: %(default)s"); +_("Create Cloud-Optimized GeoTIFFs instead of normal GeoTIFFs. Default: %(default)s"); +_("Generate OGC 3D Tiles outputs. Default: %(default)s"); +_("Override the rolling shutter readout time for your camera sensor (in milliseconds), instead of using the rolling shutter readout database. Note that not all cameras are present in the database. Set to 0 to use the database value. Default: %(default)s"); +_("Set this parameter if you want to generate a PNG rendering of the orthophoto. Default: %(default)s"); +_("Path to the file containing the ground control points used for georeferencing. The file needs to use the following format: EPSG: or <+proj definition>geo_x geo_y geo_z im_x im_y image_name [gcp_name] [extra1] [extra2]Default: %(default)s"); +_("Set feature extraction quality. Higher quality generates better features, but requires more memory and takes longer. Can be one of: %(choices)s. Default: %(default)s"); +_("Use a full 3D mesh to compute the orthophoto instead of a 2.5D mesh. This option is a bit faster and provides similar results in planar areas. Default: %(default)s"); _("Orthophoto resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also. Default: %(default)s"); -_("Ignore Ground Sampling Distance (GSD). GSD caps the maximum resolution of image outputs and resizes images when necessary, resulting in faster processing and lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality. Default: %(default)s"); -_("Radius of the overlap between submodels. After grouping images into clusters, images that are closer than this radius to a cluster are added to the cluster. This is done to ensure that neighboring submodels overlap. Default: %(default)s"); -_("Use this tag to build a DTM (Digital Terrain Model, ground only) using a simple morphological filter. Check the --dem* and --smrf* parameters for finer tuning. Default: %(default)s"); +_("The maximum output resolution of extracted video frames in pixels. Default: %(default)s"); +_("Do not use GPU acceleration, even if it's available. Default: %(default)s"); +_("Decimate the points before generating the DEM. 1 is no decimation (full quality). 100 decimates ~99%% of the points. Useful for speeding up generation of DEM results in very large datasets. Default: %(default)s"); +_("Average number of images per submodel. When splitting a large dataset into smaller submodels, images are grouped into clusters. This value regulates the number of images that each cluster should have on average. Default: %(default)s"); +_("Skips dense reconstruction and 3D model generation. It generates an orthophoto directly from the sparse reconstruction. If you just need an orthophoto and do not need a full 3D model, turn on this option. Default: %(default)s"); +_("Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. Starting with a radius equal to the output resolution, N different DEMs are generated with progressively bigger radius using the inverse distance weighted (IDW) algorithm and merged together. Remaining gaps are then merged using nearest neighbor interpolation. Default: %(default)s"); +_("Classify the point cloud outputs. You can control the behavior of this option by tweaking the --dem-* parameters. Default: %(default)s"); +_("Export the georeferenced point cloud in LAS format. Default: %(default)s"); +_("Set the radiometric calibration to perform on images. When processing multispectral and thermal images you should set this option to obtain reflectance/temperature values (otherwise you will get digital number values). [camera] applies black level, vignetting, row gradient gain/exposure compensation (if appropriate EXIF tags are found) and computes absolute temperature values. [camera+sun] is experimental, applies all the corrections of [camera], plus compensates for spectral radiance registered via a downwelling light sensor (DLS) taking in consideration the angle of the sun. Can be one of: %(choices)s. Default: %(default)s"); +_("Computes an euclidean raster map for each DEM. The map reports the distance from each cell to the nearest NODATA value (before any hole filling takes place). This can be useful to isolate the areas that have been filled. Default: %(default)s"); +_("Set a camera projection type. Manually setting a value can help improve geometric undistortion. By default the application tries to determine a lens type from the images metadata. Can be one of: %(choices)s. Default: %(default)s"); +_("Path to the image geolocation file containing the camera center coordinates used for georeferencing. If you don't have values for yaw/pitch/roll you can set them to 0. The file needs to use the following format: EPSG: or <+proj definition>image_name geo_x geo_y geo_z [yaw (degrees)] [pitch (degrees)] [roll (degrees)] [horz accuracy (meters)] [vert accuracy (meters)]Default: %(default)s"); +_("Name of dataset (i.e subfolder name within project folder). Default: %(default)s"); +_("Simple Morphological Filter elevation threshold parameter (meters). Default: %(default)s"); +_("Simple Morphological Filter window radius parameter (meters). Default: %(default)s"); +_("When processing multispectral datasets, you can specify the name of the primary band that will be used for reconstruction. It's recommended to choose a band which has sharp details and is in focus. Default: %(default)s"); +_("Set this parameter if you want to generate a Google Earth (KMZ) rendering of the orthophoto. Default: %(default)s"); +_("Set the compression to use for orthophotos. Can be one of: %(choices)s. Default: %(default)s"); +_("Path to the project folder. Your project folder should contain subfolders for each dataset. Each dataset should have an \"images\" folder."); diff --git a/locale b/locale index a5348d69..e94f6635 160000 --- a/locale +++ b/locale @@ -1 +1 @@ -Subproject commit a5348d69b500dc6547d30ef8a7b580b217efb760 +Subproject commit e94f6635542cbe783d67ff2bf77c30c5bc380d59 From e41b095a9a1f629829db6c261c4b4223401bacbc Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 23 Mar 2023 10:21:03 -0400 Subject: [PATCH 31/41] Update error message --- app/static/app/js/components/TaskListItem.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/app/js/components/TaskListItem.jsx b/app/static/app/js/components/TaskListItem.jsx index e0915ec2..25611138 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 { {showExitedWithCodeOneHints ?
      - DroneDB`, link2: `Google Drive`, open_a_topic: `${_("open a topic")}`, }}>{_("\"Process exited with code 1\" means that part of the processing failed. Sometimes it's a problem with the dataset, sometimes it can be solved by tweaking the Task Options and sometimes it might be a bug! If you need help, upload your images somewhere like %(link1)s or %(link2)s and %(open_a_topic)s on our community forum, making sure to include a copy of your task's output. Our awesome contributors will try to help you!")} + docs.opendronemap.org` }}>{_("\"Process exited with code 1\" means that part of the processing failed. Sometimes it's a problem with the dataset, sometimes it can be solved by tweaking the Task Options. Check the documentation at %(link)")}
      : ""} From 6258626102595fafd02baea31fed4842990105fb Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 23 Mar 2023 10:22:02 -0400 Subject: [PATCH 32/41] Updated translations --- .../app/js/translations/odm_autogenerated.js | 168 +++++++++--------- locale | 2 +- 2 files changed, 85 insertions(+), 85 deletions(-) diff --git a/app/static/app/js/translations/odm_autogenerated.js b/app/static/app/js/translations/odm_autogenerated.js index 9b022dfb..ef375cd8 100644 --- a/app/static/app/js/translations/odm_autogenerated.js +++ b/app/static/app/js/translations/odm_autogenerated.js @@ -1,93 +1,93 @@ // Auto-generated with extract_odm_strings.py, do not edit! -_("Skip normalization of colors across all images. Useful when processing radiometric data. Default: %(default)s"); -_("Displays version number and exits. "); -_("Automatically set a boundary using camera shot locations to limit the area of the reconstruction. This can help remove far away background artifacts (sky, background landscapes, etc.). See also --boundary. Default: %(default)s"); -_("The maximum vertex count of the output mesh. Default: %(default)s"); -_("URL to a ClusterODM instance for distributing a split-merge workflow on multiple nodes in parallel. Default: %(default)s"); -_("Radius of the overlap between submodels. After grouping images into clusters, images that are closer than this radius to a cluster are added to the cluster. This is done to ensure that neighboring submodels overlap. Default: %(default)s"); -_("Save the georeferenced point cloud in Cloud Optimized Point Cloud (COPC) format. Default: %(default)s"); -_("Choose the algorithm for extracting keypoints and computing descriptors. Can be one of: %(choices)s. Default: %(default)s"); -_("Matcher algorithm, Fast Library for Approximate Nearest Neighbors or Bag of Words. FLANN is slower, but more stable. BOW is faster, but can sometimes miss valid matches. BRUTEFORCE is very slow but robust.Can be one of: %(choices)s. Default: %(default)s"); -_("Set a value in meters for the GPS Dilution of Precision (DOP) information for all images. If your images are tagged with high precision GPS information (RTK), this value will be automatically set accordingly. You can use this option to manually set it in case the reconstruction fails. Lowering this option can sometimes help control bowling-effects over large areas. Default: %(default)s"); -_("Maximum number of frames to extract from video files for processing. Set to 0 for no limit. Default: %(default)s"); -_("Generate single file Binary glTF (GLB) textured models. Default: %(default)s"); -_("Simple Morphological Filter elevation scalar parameter. Default: %(default)s"); -_("Permanently delete all previous results and rerun the processing pipeline."); -_("Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud and remove duplicate points. Set to 0 to disable sampling. Default: %(default)s"); -_("Octree depth used in the mesh reconstruction, increase to get more vertices, recommended values are 8-12. Default: %(default)s"); -_("Export the georeferenced point cloud in CSV format. Default: %(default)s"); _("Turn off camera parameter optimization during bundle adjustment. This can be sometimes useful for improving results that exhibit doming/bowling or when images are taken with a rolling shutter camera. Default: %(default)s"); -_("Skip generation of a full 3D model. This can save time if you only need 2D results such as orthophotos and DEMs. Default: %(default)s"); -_("Generate static tiles for orthophotos and DEMs that are suitable for viewers like Leaflet or OpenLayers. Default: %(default)s"); -_("Copy output results to this folder after processing."); -_("Ignore Ground Sampling Distance (GSD). GSD caps the maximum resolution of image outputs and resizes images when necessary, resulting in faster processing and lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality. Default: %(default)s"); -_("Use this tag if you have a GCP File but want to use the EXIF information for georeferencing instead. Default: %(default)s"); -_("Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive morphological filter. Check the --dem* parameters for finer tuning. Default: %(default)s"); -_("Skip the blending of colors near seams. Default: %(default)s"); -_("Perform image matching with the nearest images based on GPS exif data. Set to 0 to match by triangulation. Default: %(default)s"); -_("End processing at this stage. Can be one of: %(choices)s. Default: %(default)s"); -_("Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s"); -_("Skip generation of the orthophoto. This can save time if you only need 3D results or DEMs. Default: %(default)s"); -_("Rerun this stage only and stop. Can be one of: %(choices)s. Default: %(default)s"); -_("Skip generation of PDF report. This can save time if you don't need a report. Default: %(default)s"); -_("Choose the structure from motion algorithm. For aerial datasets, if camera GPS positions and angles are available, triangulation can generate better results. For planar scenes captured at fixed altitude with nadir-only images, planar can be much faster. Can be one of: %(choices)s. Default: %(default)s"); -_("Delete heavy intermediate files to optimize disk space usage. This affects the ability to restart the pipeline from an intermediate stage, but allows datasets to be processed on machines that don't have sufficient disk space available. Default: %(default)s"); -_("When processing multispectral datasets, ODM will automatically align the images for each band. If the images have been postprocessed and are already aligned, use this option. Default: %(default)s"); -_("Run local bundle adjustment for every image added to the reconstruction and a global adjustment every 100 images. Speeds up reconstruction for very large datasets. Default: %(default)s"); -_("Automatically compute image masks using AI to remove the background. Experimental. Default: %(default)s"); -_("Use this tag to build a DTM (Digital Terrain Model, ground only) using a simple morphological filter. Check the --dem* and --smrf* parameters for finer tuning. Default: %(default)s"); -_("Filters the point cloud by removing points that deviate more than N standard deviations from the local mean. Set to 0 to disable filtering. Default: %(default)s"); -_("GeoJSON polygon limiting the area of the reconstruction. Can be specified either as path to a GeoJSON file or as a JSON string representing the contents of a GeoJSON file. Default: %(default)s"); -_("Automatically crop image outputs by creating a smooth buffer around the dataset boundaries, shrunk by N meters. Use 0 to disable cropping. Default: %(default)s"); -_("show this help message and exit"); -_("Set point cloud quality. Higher quality generates better, denser point clouds, but requires more memory and takes longer. Each step up in quality increases processing time roughly by a factor of 4x.Can be one of: %(choices)s. Default: %(default)s"); -_("Choose what to merge in the merge step in a split dataset. By default all available outputs are merged. Options: %(choices)s. Default: %(default)s"); -_("Keep faces in the mesh that are not seen in any camera. Default: %(default)s"); -_("Set this parameter if you want a striped GeoTIFF. Default: %(default)s"); -_("Path to the image groups file that controls how images should be split into groups. The file needs to use the following format: image_name group_nameDefault: %(default)s"); -_("Automatically compute image masks using AI to remove the sky. Experimental. Default: %(default)s"); -_("Specify the distance between camera shot locations and the outer edge of the boundary when computing the boundary with --auto-boundary. Set to 0 to automatically choose a value. Default: %(default)s"); -_("Reduce the memory usage needed for depthmap fusion by splitting large scenes into tiles. Turn this on if your machine doesn't have much RAM and/or you've set --pc-quality to high or ultra. Experimental. Default: %(default)s"); -_("Use the camera parameters computed from another dataset instead of calculating them. Can be specified either as path to a cameras.json file or as a JSON string representing the contents of a cameras.json file. Default: %(default)s"); -_("Generate OBJs that have a single material and a single texture file instead of multiple ones. Default: %(default)s"); -_("Use images' GPS exif data for reconstruction, even if there are GCPs present.This flag is useful if you have high precision GPS measurements. If there are no GCPs, this flag does nothing. Default: %(default)s"); -_("Simple Morphological Filter slope parameter (rise over run). Default: %(default)s"); -_("Build orthophoto overviews for faster display in programs such as QGIS. Default: %(default)s"); -_("Generates a polygon around the cropping area that cuts the orthophoto around the edges of features. This polygon can be useful for stitching seamless mosaics with multiple overlapping orthophotos. Default: %(default)s"); -_("Turn on rolling shutter correction. If the camera has a rolling shutter and the images were taken in motion, you can turn on this option to improve the accuracy of the results. See also --rolling-shutter-readout. Default: %(default)s"); -_("Skip alignment of submodels in split-merge. Useful if GPS is good enough on very large datasets. Default: %(default)s"); _("Geometric estimates improve the accuracy of the point cloud by computing geometrically consistent depthmaps but may not be usable in larger datasets. This flag disables geometric estimates. Default: %(default)s"); -_("The maximum number of processes to use in various processes. Peak memory requirement is ~1GB per thread and 2 megapixel image resolution. Default: %(default)s"); -_("Path to a GeoTIFF DEM or a LAS/LAZ point cloud that the reconstruction outputs should be automatically aligned to. Experimental. Default: %(default)s"); -_("DSM/DTM resolution in cm / pixel. Note that this value is capped to 2x the ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also. Default: %(default)s"); -_("Minimum number of features to extract per image. More features can be useful for finding more matches between images, potentially allowing the reconstruction of areas with little overlap or insufficient features. More features also slow down processing. Default: %(default)s"); -_("Rerun processing from this stage. Can be one of: %(choices)s. Default: %(default)s"); +_("Skip normalization of colors across all images. Useful when processing radiometric data. Default: %(default)s"); +_("show this help message and exit"); +_("Set this parameter if you want a striped GeoTIFF. Default: %(default)s"); +_("Run local bundle adjustment for every image added to the reconstruction and a global adjustment every 100 images. Speeds up reconstruction for very large datasets. Default: %(default)s"); _("Perform ground rectification on the point cloud. This means that wrongly classified ground points will be re-classified and gaps will be filled. Useful for generating DTMs. Default: %(default)s"); -_("Create Cloud-Optimized GeoTIFFs instead of normal GeoTIFFs. Default: %(default)s"); -_("Generate OGC 3D Tiles outputs. Default: %(default)s"); -_("Override the rolling shutter readout time for your camera sensor (in milliseconds), instead of using the rolling shutter readout database. Note that not all cameras are present in the database. Set to 0 to use the database value. Default: %(default)s"); -_("Set this parameter if you want to generate a PNG rendering of the orthophoto. Default: %(default)s"); +_("Radius of the overlap between submodels. After grouping images into clusters, images that are closer than this radius to a cluster are added to the cluster. This is done to ensure that neighboring submodels overlap. Default: %(default)s"); +_("Choose what to merge in the merge step in a split dataset. By default all available outputs are merged. Options: %(choices)s. Default: %(default)s"); _("Path to the file containing the ground control points used for georeferencing. The file needs to use the following format: EPSG: or <+proj definition>geo_x geo_y geo_z im_x im_y image_name [gcp_name] [extra1] [extra2]Default: %(default)s"); -_("Set feature extraction quality. Higher quality generates better features, but requires more memory and takes longer. Can be one of: %(choices)s. Default: %(default)s"); -_("Use a full 3D mesh to compute the orthophoto instead of a 2.5D mesh. This option is a bit faster and provides similar results in planar areas. Default: %(default)s"); -_("Orthophoto resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also. Default: %(default)s"); -_("The maximum output resolution of extracted video frames in pixels. Default: %(default)s"); -_("Do not use GPU acceleration, even if it's available. Default: %(default)s"); -_("Decimate the points before generating the DEM. 1 is no decimation (full quality). 100 decimates ~99%% of the points. Useful for speeding up generation of DEM results in very large datasets. Default: %(default)s"); -_("Average number of images per submodel. When splitting a large dataset into smaller submodels, images are grouped into clusters. This value regulates the number of images that each cluster should have on average. Default: %(default)s"); -_("Skips dense reconstruction and 3D model generation. It generates an orthophoto directly from the sparse reconstruction. If you just need an orthophoto and do not need a full 3D model, turn on this option. Default: %(default)s"); -_("Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. Starting with a radius equal to the output resolution, N different DEMs are generated with progressively bigger radius using the inverse distance weighted (IDW) algorithm and merged together. Remaining gaps are then merged using nearest neighbor interpolation. Default: %(default)s"); -_("Classify the point cloud outputs. You can control the behavior of this option by tweaking the --dem-* parameters. Default: %(default)s"); -_("Export the georeferenced point cloud in LAS format. Default: %(default)s"); -_("Set the radiometric calibration to perform on images. When processing multispectral and thermal images you should set this option to obtain reflectance/temperature values (otherwise you will get digital number values). [camera] applies black level, vignetting, row gradient gain/exposure compensation (if appropriate EXIF tags are found) and computes absolute temperature values. [camera+sun] is experimental, applies all the corrections of [camera], plus compensates for spectral radiance registered via a downwelling light sensor (DLS) taking in consideration the angle of the sun. Can be one of: %(choices)s. Default: %(default)s"); -_("Computes an euclidean raster map for each DEM. The map reports the distance from each cell to the nearest NODATA value (before any hole filling takes place). This can be useful to isolate the areas that have been filled. Default: %(default)s"); -_("Set a camera projection type. Manually setting a value can help improve geometric undistortion. By default the application tries to determine a lens type from the images metadata. Can be one of: %(choices)s. Default: %(default)s"); -_("Path to the image geolocation file containing the camera center coordinates used for georeferencing. If you don't have values for yaw/pitch/roll you can set them to 0. The file needs to use the following format: EPSG: or <+proj definition>image_name geo_x geo_y geo_z [yaw (degrees)] [pitch (degrees)] [roll (degrees)] [horz accuracy (meters)] [vert accuracy (meters)]Default: %(default)s"); -_("Name of dataset (i.e subfolder name within project folder). Default: %(default)s"); +_("Specify the distance between camera shot locations and the outer edge of the boundary when computing the boundary with --auto-boundary. Set to 0 to automatically choose a value. Default: %(default)s"); _("Simple Morphological Filter elevation threshold parameter (meters). Default: %(default)s"); -_("Simple Morphological Filter window radius parameter (meters). Default: %(default)s"); -_("When processing multispectral datasets, you can specify the name of the primary band that will be used for reconstruction. It's recommended to choose a band which has sharp details and is in focus. Default: %(default)s"); -_("Set this parameter if you want to generate a Google Earth (KMZ) rendering of the orthophoto. Default: %(default)s"); _("Set the compression to use for orthophotos. Can be one of: %(choices)s. Default: %(default)s"); +_("Automatically compute image masks using AI to remove the sky. Experimental. Default: %(default)s"); +_("Delete heavy intermediate files to optimize disk space usage. This affects the ability to restart the pipeline from an intermediate stage, but allows datasets to be processed on machines that don't have sufficient disk space available. Default: %(default)s"); +_("Permanently delete all previous results and rerun the processing pipeline."); +_("The maximum vertex count of the output mesh. Default: %(default)s"); +_("Save the georeferenced point cloud in Cloud Optimized Point Cloud (COPC) format. Default: %(default)s"); +_("GeoJSON polygon limiting the area of the reconstruction. Can be specified either as path to a GeoJSON file or as a JSON string representing the contents of a GeoJSON file. Default: %(default)s"); +_("Create Cloud-Optimized GeoTIFFs instead of normal GeoTIFFs. Default: %(default)s"); +_("Skip the blending of colors near seams. Default: %(default)s"); +_("Skip generation of a full 3D model. This can save time if you only need 2D results such as orthophotos and DEMs. Default: %(default)s"); +_("Automatically set a boundary using camera shot locations to limit the area of the reconstruction. This can help remove far away background artifacts (sky, background landscapes, etc.). See also --boundary. Default: %(default)s"); +_("Export the georeferenced point cloud in LAS format. Default: %(default)s"); _("Path to the project folder. Your project folder should contain subfolders for each dataset. Each dataset should have an \"images\" folder."); +_("Path to the image groups file that controls how images should be split into groups. The file needs to use the following format: image_name group_nameDefault: %(default)s"); +_("Override the rolling shutter readout time for your camera sensor (in milliseconds), instead of using the rolling shutter readout database. Note that not all cameras are present in the database. Set to 0 to use the database value. Default: %(default)s"); +_("Name of dataset (i.e subfolder name within project folder). Default: %(default)s"); +_("Copy output results to this folder after processing."); +_("Choose the algorithm for extracting keypoints and computing descriptors. Can be one of: %(choices)s. Default: %(default)s"); +_("Simple Morphological Filter slope parameter (rise over run). Default: %(default)s"); +_("Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s"); +_("Skip alignment of submodels in split-merge. Useful if GPS is good enough on very large datasets. Default: %(default)s"); +_("Turn on rolling shutter correction. If the camera has a rolling shutter and the images were taken in motion, you can turn on this option to improve the accuracy of the results. See also --rolling-shutter-readout. Default: %(default)s"); +_("Skip generation of PDF report. This can save time if you don't need a report. Default: %(default)s"); +_("Generate OBJs that have a single material and a single texture file instead of multiple ones. Default: %(default)s"); +_("Computes an euclidean raster map for each DEM. The map reports the distance from each cell to the nearest NODATA value (before any hole filling takes place). This can be useful to isolate the areas that have been filled. Default: %(default)s"); +_("Use this tag to build a DTM (Digital Terrain Model, ground only) using a simple morphological filter. Check the --dem* and --smrf* parameters for finer tuning. Default: %(default)s"); +_("Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud and remove duplicate points. Set to 0 to disable sampling. Default: %(default)s"); +_("Reduce the memory usage needed for depthmap fusion by splitting large scenes into tiles. Turn this on if your machine doesn't have much RAM and/or you've set --pc-quality to high or ultra. Experimental. Default: %(default)s"); +_("When processing multispectral datasets, ODM will automatically align the images for each band. If the images have been postprocessed and are already aligned, use this option. Default: %(default)s"); +_("Export the georeferenced point cloud in CSV format. Default: %(default)s"); +_("DSM/DTM resolution in cm / pixel. Note that this value is capped to 2x the ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also. Default: %(default)s"); +_("Set the radiometric calibration to perform on images. When processing multispectral and thermal images you should set this option to obtain reflectance/temperature values (otherwise you will get digital number values). [camera] applies black level, vignetting, row gradient gain/exposure compensation (if appropriate EXIF tags are found) and computes absolute temperature values. [camera+sun] is experimental, applies all the corrections of [camera], plus compensates for spectral radiance registered via a downwelling light sensor (DLS) taking in consideration the angle of the sun. Can be one of: %(choices)s. Default: %(default)s"); +_("The maximum number of processes to use in various processes. Peak memory requirement is ~1GB per thread and 2 megapixel image resolution. Default: %(default)s"); +_("Automatically crop image outputs by creating a smooth buffer around the dataset boundaries, shrunk by N meters. Use 0 to disable cropping. Default: %(default)s"); +_("Simple Morphological Filter elevation scalar parameter. Default: %(default)s"); +_("Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. Starting with a radius equal to the output resolution, N different DEMs are generated with progressively bigger radius using the inverse distance weighted (IDW) algorithm and merged together. Remaining gaps are then merged using nearest neighbor interpolation. Default: %(default)s"); +_("Generate static tiles for orthophotos and DEMs that are suitable for viewers like Leaflet or OpenLayers. Default: %(default)s"); +_("Generates a polygon around the cropping area that cuts the orthophoto around the edges of features. This polygon can be useful for stitching seamless mosaics with multiple overlapping orthophotos. Default: %(default)s"); +_("Set feature extraction quality. Higher quality generates better features, but requires more memory and takes longer. Can be one of: %(choices)s. Default: %(default)s"); +_("Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive morphological filter. Check the --dem* parameters for finer tuning. Default: %(default)s"); +_("Set a camera projection type. Manually setting a value can help improve geometric undistortion. By default the application tries to determine a lens type from the images metadata. Can be one of: %(choices)s. Default: %(default)s"); +_("Use images' GPS exif data for reconstruction, even if there are GCPs present.This flag is useful if you have high precision GPS measurements. If there are no GCPs, this flag does nothing. Default: %(default)s"); +_("End processing at this stage. Can be one of: %(choices)s. Default: %(default)s"); +_("Automatically compute image masks using AI to remove the background. Experimental. Default: %(default)s"); +_("Path to the image geolocation file containing the camera center coordinates used for georeferencing. If you don't have values for yaw/pitch/roll you can set them to 0. The file needs to use the following format: EPSG: or <+proj definition>image_name geo_x geo_y geo_z [yaw (degrees)] [pitch (degrees)] [roll (degrees)] [horz accuracy (meters)] [vert accuracy (meters)]Default: %(default)s"); +_("Matcher algorithm, Fast Library for Approximate Nearest Neighbors or Bag of Words. FLANN is slower, but more stable. BOW is faster, but can sometimes miss valid matches. BRUTEFORCE is very slow but robust.Can be one of: %(choices)s. Default: %(default)s"); +_("Generate OGC 3D Tiles outputs. Default: %(default)s"); +_("Use this tag if you have a GCP File but want to use the EXIF information for georeferencing instead. Default: %(default)s"); +_("Do not use GPU acceleration, even if it's available. Default: %(default)s"); +_("Simple Morphological Filter window radius parameter (meters). Default: %(default)s"); +_("Path to a GeoTIFF DEM or a LAS/LAZ point cloud that the reconstruction outputs should be automatically aligned to. Experimental. Default: %(default)s"); +_("Keep faces in the mesh that are not seen in any camera. Default: %(default)s"); +_("Set this parameter if you want to generate a Google Earth (KMZ) rendering of the orthophoto. Default: %(default)s"); +_("Classify the point cloud outputs. You can control the behavior of this option by tweaking the --dem-* parameters. Default: %(default)s"); +_("Average number of images per submodel. When splitting a large dataset into smaller submodels, images are grouped into clusters. This value regulates the number of images that each cluster should have on average. Default: %(default)s"); +_("Generate single file Binary glTF (GLB) textured models. Default: %(default)s"); +_("Rerun processing from this stage. Can be one of: %(choices)s. Default: %(default)s"); +_("Rerun this stage only and stop. Can be one of: %(choices)s. Default: %(default)s"); +_("Decimate the points before generating the DEM. 1 is no decimation (full quality). 100 decimates ~99%% of the points. Useful for speeding up generation of DEM results in very large datasets. Default: %(default)s"); +_("When processing multispectral datasets, you can specify the name of the primary band that will be used for reconstruction. It's recommended to choose a band which has sharp details and is in focus. Default: %(default)s"); +_("Perform image matching with the nearest images based on GPS exif data. Set to 0 to match by triangulation. Default: %(default)s"); +_("Use the camera parameters computed from another dataset instead of calculating them. Can be specified either as path to a cameras.json file or as a JSON string representing the contents of a cameras.json file. Default: %(default)s"); +_("URL to a ClusterODM instance for distributing a split-merge workflow on multiple nodes in parallel. Default: %(default)s"); +_("The maximum output resolution of extracted video frames in pixels. Default: %(default)s"); +_("Ignore Ground Sampling Distance (GSD). GSD caps the maximum resolution of image outputs and resizes images when necessary, resulting in faster processing and lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality. Default: %(default)s"); +_("Filters the point cloud by removing points that deviate more than N standard deviations from the local mean. Set to 0 to disable filtering. Default: %(default)s"); +_("Build orthophoto overviews for faster display in programs such as QGIS. Default: %(default)s"); +_("Octree depth used in the mesh reconstruction, increase to get more vertices, recommended values are 8-12. Default: %(default)s"); +_("Use a full 3D mesh to compute the orthophoto instead of a 2.5D mesh. This option is a bit faster and provides similar results in planar areas. Default: %(default)s"); +_("Skips dense reconstruction and 3D model generation. It generates an orthophoto directly from the sparse reconstruction. If you just need an orthophoto and do not need a full 3D model, turn on this option. Default: %(default)s"); +_("Choose the structure from motion algorithm. For aerial datasets, if camera GPS positions and angles are available, triangulation can generate better results. For planar scenes captured at fixed altitude with nadir-only images, planar can be much faster. Can be one of: %(choices)s. Default: %(default)s"); +_("Orthophoto resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also. Default: %(default)s"); +_("Displays version number and exits. "); +_("Maximum number of frames to extract from video files for processing. Set to 0 for no limit. Default: %(default)s"); +_("Set a value in meters for the GPS Dilution of Precision (DOP) information for all images. If your images are tagged with high precision GPS information (RTK), this value will be automatically set accordingly. You can use this option to manually set it in case the reconstruction fails. Lowering this option can sometimes help control bowling-effects over large areas. Default: %(default)s"); +_("Skip generation of the orthophoto. This can save time if you only need 3D results or DEMs. Default: %(default)s"); +_("Minimum number of features to extract per image. More features can be useful for finding more matches between images, potentially allowing the reconstruction of areas with little overlap or insufficient features. More features also slow down processing. Default: %(default)s"); +_("Set this parameter if you want to generate a PNG rendering of the orthophoto. Default: %(default)s"); +_("Set point cloud quality. Higher quality generates better, denser point clouds, but requires more memory and takes longer. Each step up in quality increases processing time roughly by a factor of 4x.Can be one of: %(choices)s. Default: %(default)s"); diff --git a/locale b/locale index e94f6635..12f8546a 160000 --- a/locale +++ b/locale @@ -1 +1 @@ -Subproject commit e94f6635542cbe783d67ff2bf77c30c5bc380d59 +Subproject commit 12f8546a1779a1e86254a806a2c88661cee07d84 From ed5ac98d06ac7f0d48d06a5e389271ffaf33314b Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 23 Mar 2023 13:31:07 -0400 Subject: [PATCH 33/41] Drop ImageUpload model --- app/admin.py | 8 +--- app/api/imageuploads.py | 15 +----- app/api/tasks.py | 14 ++---- app/migrations/0012_public_task_uuids.py | 9 ++-- app/migrations/0013_public_task_uuids.py | 5 +- app/migrations/0015_public_task_uuids.py | 54 ---------------------- app/migrations/0016_public_task_uuids.py | 2 +- app/migrations/0026_update_images_count.py | 2 +- app/migrations/0029_auto_20190907_1348.py | 4 +- app/migrations/0031_auto_20210610_1850.py | 4 +- app/migrations/0034_delete_imageupload.py | 16 +++++++ app/models/__init__.py | 4 +- app/models/image_upload.py | 21 --------- app/models/task.py | 54 ++++++++++++++-------- app/tests/test_api_task.py | 15 ++---- contrib/Hard_Recovery_Guide.md | 17 ++----- coreplugins/cloudimport/api_views.py | 6 +-- coreplugins/dronedb/api_views.py | 6 +-- coreplugins/openaerialmap/api.py | 8 ++-- 19 files changed, 89 insertions(+), 175 deletions(-) delete mode 100644 app/migrations/0015_public_task_uuids.py create mode 100644 app/migrations/0034_delete_imageupload.py delete mode 100644 app/models/image_upload.py diff --git a/app/admin.py b/app/admin.py index c4c61a04..029dd3cb 100644 --- a/app/admin.py +++ b/app/admin.py @@ -16,7 +16,7 @@ from app.models import Preset from app.models import Plugin from app.plugins import get_plugin_by_name, enable_plugin, disable_plugin, delete_plugin, valid_plugin, \ get_plugins_persistent_path, clear_plugins_cache, init_plugins -from .models import Project, Task, ImageUpload, Setting, Theme +from .models import Project, Task, Setting, Theme from django import forms from codemirror2.widgets import CodeMirrorEditor from webodm import settings @@ -37,12 +37,6 @@ class TaskAdmin(admin.ModelAdmin): admin.site.register(Task, TaskAdmin) - -class ImageUploadAdmin(admin.ModelAdmin): - readonly_fields = ('image',) - -admin.site.register(ImageUpload, ImageUploadAdmin) - admin.site.register(Preset, admin.ModelAdmin) diff --git a/app/api/imageuploads.py b/app/api/imageuploads.py index 6fa2d8a0..64efabd5 100644 --- a/app/api/imageuploads.py +++ b/app/api/imageuploads.py @@ -4,7 +4,6 @@ import math from .tasks import TaskNestedView from rest_framework import exceptions -from app.models import ImageUpload from app.models.task import assets_directory_path from PIL import Image, ImageDraw, ImageOps from django.http import HttpResponse @@ -33,12 +32,7 @@ class Thumbnail(TaskNestedView): Generate a thumbnail on the fly for a particular task's image """ task = self.get_and_check_task(request, pk) - image = ImageUpload.objects.filter(task=task, image=assets_directory_path(task.id, task.project.id, image_filename)).first() - - if image is None: - raise exceptions.NotFound() - - image_path = image.path() + image_path = task.get_image_path(image_filename) if not os.path.isfile(image_path): raise exceptions.NotFound() @@ -146,12 +140,7 @@ class ImageDownload(TaskNestedView): Download a task's image """ task = self.get_and_check_task(request, pk) - image = ImageUpload.objects.filter(task=task, image=assets_directory_path(task.id, task.project.id, image_filename)).first() - - if image is None: - raise exceptions.NotFound() - - image_path = image.path() + image_path = task.get_image_path(image_filename) if not os.path.isfile(image_path): raise exceptions.NotFound() diff --git a/app/api/tasks.py b/app/api/tasks.py index 7e6e21b3..f3761dfb 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -179,7 +179,7 @@ class TaskViewSet(viewsets.ViewSet): raise exceptions.NotFound() task.partial = False - task.images_count = models.ImageUpload.objects.filter(task=task).count() + task.images_count = len(task.scan_images()) if task.images_count < 2: raise exceptions.ValidationError(detail=_("You need to upload at least 2 images before commit")) @@ -206,11 +206,8 @@ class TaskViewSet(viewsets.ViewSet): if len(files) == 0: raise exceptions.ValidationError(detail=_("No files uploaded")) - with transaction.atomic(): - for image in files: - models.ImageUpload.objects.create(task=task, image=image) - - task.images_count = models.ImageUpload.objects.filter(task=task).count() + task.handle_images_upload(files) + task.images_count = len(task.scan_images()) # Update other parameters such as processing node, task name, etc. serializer = TaskSerializer(task, data=request.data, partial=True) serializer.is_valid(raise_exception=True) @@ -256,9 +253,8 @@ class TaskViewSet(viewsets.ViewSet): task = models.Task.objects.create(project=project, pending_action=pending_actions.RESIZE if 'resize_to' in request.data else None) - for image in files: - models.ImageUpload.objects.create(task=task, image=image) - task.images_count = len(files) + task.handle_images_upload(files) + task.images_count = len(task.scan_images()) # Update other parameters such as processing node, task name, etc. serializer = TaskSerializer(task, data=request.data, partial=True) diff --git a/app/migrations/0012_public_task_uuids.py b/app/migrations/0012_public_task_uuids.py index 7b8f7956..73919178 100644 --- a/app/migrations/0012_public_task_uuids.py +++ b/app/migrations/0012_public_task_uuids.py @@ -8,17 +8,14 @@ import uuid, os, pickle, tempfile from webodm import settings tasks = [] -imageuploads = [] task_ids = {} # map old task IDs --> new task IDs def dump(apps, schema_editor): - global tasks, imageuploads, task_ids + global tasks, task_ids Task = apps.get_model('app', 'Task') - ImageUpload = apps.get_model('app', 'ImageUpload') tasks = list(Task.objects.all().values('id', 'project')) - imageuploads = list(ImageUpload.objects.all().values('id', 'task')) # Generate UUIDs for task in tasks: @@ -31,9 +28,9 @@ def dump(apps, schema_editor): task_ids[task['id']] = new_id tmp_path = os.path.join(tempfile.gettempdir(), "public_task_uuids_migration.pickle") - pickle.dump((tasks, imageuploads, task_ids), open(tmp_path, 'wb')) + pickle.dump((tasks, task_ids), open(tmp_path, 'wb')) - if len(tasks) > 0: print("Dumped tasks and imageuploads") + if len(tasks) > 0: print("Dumped tasks") class Migration(migrations.Migration): diff --git a/app/migrations/0013_public_task_uuids.py b/app/migrations/0013_public_task_uuids.py index 321ecfc3..7486a82e 100644 --- a/app/migrations/0013_public_task_uuids.py +++ b/app/migrations/0013_public_task_uuids.py @@ -8,7 +8,6 @@ import uuid, os, pickle, tempfile from webodm import settings tasks = [] -imageuploads = [] task_ids = {} # map old task IDs --> new task IDs def task_path(project_id, task_id): @@ -44,10 +43,10 @@ def create_uuids(apps, schema_editor): def restore(apps, schema_editor): - global tasks, imageuploads, task_ids + global tasks, task_ids tmp_path = os.path.join(tempfile.gettempdir(), "public_task_uuids_migration.pickle") - tasks, imageuploads, task_ids = pickle.load(open(tmp_path, 'rb')) + tasks, task_ids = pickle.load(open(tmp_path, 'rb')) class Migration(migrations.Migration): diff --git a/app/migrations/0015_public_task_uuids.py b/app/migrations/0015_public_task_uuids.py deleted file mode 100644 index df60ae54..00000000 --- a/app/migrations/0015_public_task_uuids.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.1 on 2017-11-30 15:41 -from __future__ import unicode_literals - -from django.db import migrations, models -import os, pickle, tempfile - -from webodm import settings - -tasks = [] -imageuploads = [] -task_ids = {} # map old task IDs --> new task IDs - - -def restoreImageUploadFks(apps, schema_editor): - global imageuploads, task_ids - - ImageUpload = apps.get_model('app', 'ImageUpload') - Task = apps.get_model('app', 'Task') - - for img in imageuploads: - i = ImageUpload.objects.get(pk=img['id']) - old_image_path = i.image.name - task_id = task_ids[img['task']] - - # project/2/task/5/DJI_0032.JPG --> project/2/task//DJI_0032.JPG - dirs, filename = os.path.split(old_image_path) - head, tail = os.path.split(dirs) - new_image_path = os.path.join(head, str(task_id), filename) - - i.task = Task.objects.get(id=task_id) - i.image.name = new_image_path - i.save() - - print("{} --> {} (Task {})".format(old_image_path, new_image_path, str(task_id))) - - -def restore(apps, schema_editor): - global tasks, imageuploads, task_ids - - tmp_path = os.path.join(tempfile.gettempdir(), "public_task_uuids_migration.pickle") - tasks, imageuploads, task_ids = pickle.load(open(tmp_path, 'rb')) - - -class Migration(migrations.Migration): - - dependencies = [ - ('app', '0014_public_task_uuids'), - ] - - operations = [ - migrations.RunPython(restore), - migrations.RunPython(restoreImageUploadFks), - ] diff --git a/app/migrations/0016_public_task_uuids.py b/app/migrations/0016_public_task_uuids.py index 7022496c..cc34e4e4 100644 --- a/app/migrations/0016_public_task_uuids.py +++ b/app/migrations/0016_public_task_uuids.py @@ -9,7 +9,7 @@ from webodm import settings class Migration(migrations.Migration): dependencies = [ - ('app', '0015_public_task_uuids'), + ('app', '0014_public_task_uuids'), ] operations = [ diff --git a/app/migrations/0026_update_images_count.py b/app/migrations/0026_update_images_count.py index 6c55b8f8..8cde8ebf 100644 --- a/app/migrations/0026_update_images_count.py +++ b/app/migrations/0026_update_images_count.py @@ -10,7 +10,7 @@ def update_images_count(apps, schema_editor): for t in Task.objects.all(): print("Updating {}".format(t)) - t.images_count = t.imageupload_set.count() + t.images_count = len(t.scan_images()) t.save() diff --git a/app/migrations/0029_auto_20190907_1348.py b/app/migrations/0029_auto_20190907_1348.py index 09b6e0a1..9977a4d2 100644 --- a/app/migrations/0029_auto_20190907_1348.py +++ b/app/migrations/0029_auto_20190907_1348.py @@ -1,6 +1,6 @@ # Generated by Django 2.1.11 on 2019-09-07 13:48 -import app.models.image_upload +import app.models from django.db import migrations, models @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='imageupload', name='image', - field=models.ImageField(help_text='File uploaded by a user', max_length=512, upload_to=app.models.image_upload.image_directory_path), + field=models.ImageField(help_text='File uploaded by a user', max_length=512, upload_to=app.models.image_directory_path), ), ] diff --git a/app/migrations/0031_auto_20210610_1850.py b/app/migrations/0031_auto_20210610_1850.py index 4d351f81..9ae9c16c 100644 --- a/app/migrations/0031_auto_20210610_1850.py +++ b/app/migrations/0031_auto_20210610_1850.py @@ -1,7 +1,7 @@ # Generated by Django 2.1.15 on 2021-06-10 18:50 -import app.models.image_upload import app.models.task +from app.models import image_directory_path import colorfield.fields from django.conf import settings import django.contrib.gis.db.models.fields @@ -60,7 +60,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='imageupload', name='image', - field=models.ImageField(help_text='File uploaded by a user', max_length=512, upload_to=app.models.image_upload.image_directory_path, verbose_name='Image'), + field=models.ImageField(help_text='File uploaded by a user', max_length=512, upload_to=image_directory_path, verbose_name='Image'), ), migrations.AlterField( model_name='imageupload', diff --git a/app/migrations/0034_delete_imageupload.py b/app/migrations/0034_delete_imageupload.py new file mode 100644 index 00000000..d2227fee --- /dev/null +++ b/app/migrations/0034_delete_imageupload.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2.27 on 2023-03-23 17:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0033_auto_20230307_1532'), + ] + + operations = [ + migrations.DeleteModel( + name='ImageUpload', + ), + ] diff --git a/app/models/__init__.py b/app/models/__init__.py index dc0ababc..b7434b5d 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,4 +1,3 @@ -from .image_upload import ImageUpload, image_directory_path from .project import Project from .task import Task, validate_task_options, gcp_directory_path from .preset import Preset @@ -7,3 +6,6 @@ from .setting import Setting from .plugin_datum import PluginDatum from .plugin import Plugin +# deprecated +def image_directory_path(image_upload, filename): + raise Exception("Deprecated") \ No newline at end of file diff --git a/app/models/image_upload.py b/app/models/image_upload.py deleted file mode 100644 index 8ccbdb79..00000000 --- a/app/models/image_upload.py +++ /dev/null @@ -1,21 +0,0 @@ -from .task import Task, assets_directory_path -from django.db import models -from django.utils.translation import gettext_lazy as _ - -def image_directory_path(image_upload, filename): - return assets_directory_path(image_upload.task.id, image_upload.task.project.id, filename) - - -class ImageUpload(models.Model): - task = models.ForeignKey(Task, on_delete=models.CASCADE, help_text=_("Task this image belongs to"), verbose_name=_("Task")) - image = models.ImageField(upload_to=image_directory_path, help_text=_("File uploaded by a user"), max_length=512, verbose_name=_("Image")) - - def __str__(self): - return self.image.name - - def path(self): - return self.image.path - - class Meta: - verbose_name = _("Image Upload") - verbose_name_plural = _("Image Uploads") \ No newline at end of file diff --git a/app/models/task.py b/app/models/task.py index 79e4092a..471b56a5 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -21,6 +21,7 @@ from django.contrib.gis.gdal import GDALRaster from django.contrib.gis.gdal import OGRGeometry from django.contrib.gis.geos import GEOSGeometry from django.contrib.postgres import fields +from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.exceptions import ValidationError, SuspiciousFileOperation from django.db import models from django.db import transaction @@ -310,15 +311,6 @@ class Task(models.Model): shutil.move(old_task_folder, new_task_folder_parent) logger.info("Moved task folder from {} to {}".format(old_task_folder, new_task_folder)) - - with transaction.atomic(): - for img in self.imageupload_set.all(): - prev_name = img.image.name - img.image.name = assets_directory_path(self.id, new_project_id, - os.path.basename(img.image.name)) - logger.info("Changing {} to {}".format(prev_name, img)) - img.save() - else: logger.warning("Project changed for task {}, but either {} doesn't exist, or {} already exists. This doesn't look right, so we will not move any files.".format(self, old_task_folder, @@ -430,16 +422,6 @@ class Task(models.Model): logger.info("Duplicating {} to {}".format(self, task)) - for img in self.imageupload_set.all(): - img.pk = None - img.task = task - - prev_name = img.image.name - img.image.name = assets_directory_path(task.id, task.project.id, - os.path.basename(img.image.name)) - - img.save() - if os.path.isdir(self.task_path()): try: # Try to use hard links first @@ -629,7 +611,8 @@ class Task(models.Model): if not self.uuid and self.pending_action is None and self.status is None: logger.info("Processing... {}".format(self)) - images = [image.path() for image in self.imageupload_set.all()] + images_path = self.task_path() + images = [os.path.join(images_path, i) for i in self.scan_images()] # Track upload progress, but limit the number of DB updates # to every 2 seconds (and always record the 100% progress) @@ -1122,3 +1105,34 @@ class Task(models.Model): pass else: raise + + def scan_images(self): + tp = self.task_path() + try: + return [e.name for e in os.scandir(tp) if e.is_file()] + except: + return [] + + def get_image_path(self, filename): + p = self.task_path(filename) + return path_traversal_check(p, self.task_path()) + + def handle_images_upload(self, files): + for file in files: + name = file.name + if name is None: + continue + + tp = self.task_path() + if not os.path.exists(tp): + os.makedirs(tp, exist_ok=True) + + dst_path = self.get_image_path(name) + + with open(dst_path, 'wb+') as fd: + if isinstance(file, InMemoryUploadedFile): + for chunk in file.chunks(): + fd.write(chunk) + else: + with open(file.temporary_file_path(), 'rb') as f: + copyfileobj(f, fd) \ No newline at end of file diff --git a/app/tests/test_api_task.py b/app/tests/test_api_task.py index 256b766a..36881c5a 100644 --- a/app/tests/test_api_task.py +++ b/app/tests/test_api_task.py @@ -22,7 +22,7 @@ from app import pending_actions from app.api.formulas import algos, get_camera_filters_for from app.api.tiler import ZOOM_EXTRA_LEVELS from app.cogeo import valid_cogeo -from app.models import Project, Task, ImageUpload +from app.models import Project, Task from app.models.task import task_directory_path, full_task_directory_path, TaskInterruptedException from app.plugins.signals import task_completed, task_removed, task_removing from app.tests.classes import BootTransactionTestCase @@ -239,7 +239,7 @@ class TestApiTask(BootTransactionTestCase): self.assertEqual(task.running_progress, 0.0) # Two images should have been uploaded - self.assertTrue(ImageUpload.objects.filter(task=task).count() == 2) + self.assertEqual(len(task.scan_images()), 2) # Can_rerun_from should be an empty list self.assertTrue(len(res.data['can_rerun_from']) == 0) @@ -797,7 +797,7 @@ class TestApiTask(BootTransactionTestCase): # Has been removed along with assets self.assertFalse(Task.objects.filter(pk=task.id).exists()) - self.assertFalse(ImageUpload.objects.filter(task=task).exists()) + self.assertEqual(len(task.scan_images()), 0) task_assets_path = os.path.join(settings.MEDIA_ROOT, task_directory_path(task.id, task.project.id)) self.assertFalse(os.path.exists(task_assets_path)) @@ -881,9 +881,7 @@ class TestApiTask(BootTransactionTestCase): # Reassigning the task to another project should move its assets self.assertTrue(os.path.exists(full_task_directory_path(task.id, project.id))) - self.assertTrue(len(task.imageupload_set.all()) == 2) - for image in task.imageupload_set.all(): - self.assertTrue('project/{}/'.format(project.id) in image.image.path) + self.assertTrue(len(task.scan_images()) == 2) task.project = other_project task.save() @@ -891,9 +889,6 @@ class TestApiTask(BootTransactionTestCase): self.assertFalse(os.path.exists(full_task_directory_path(task.id, project.id))) self.assertTrue(os.path.exists(full_task_directory_path(task.id, other_project.id))) - for image in task.imageupload_set.all(): - self.assertTrue('project/{}/'.format(other_project.id) in image.image.path) - # Restart node-odm as to not generate orthophotos testWatch.clear() with start_processing_node(["--test_skip_orthophotos"]): @@ -953,7 +948,7 @@ class TestApiTask(BootTransactionTestCase): new_task = Task.objects.get(pk=new_task_id) # New task has same number of image uploads - self.assertEqual(task.imageupload_set.count(), new_task.imageupload_set.count()) + self.assertEqual(len(task.scan_images()), len(new_task.scan_images())) # Directories have been created self.assertTrue(os.path.exists(new_task.task_path())) diff --git a/contrib/Hard_Recovery_Guide.md b/contrib/Hard_Recovery_Guide.md index ba87784f..7f97d6e2 100644 --- a/contrib/Hard_Recovery_Guide.md +++ b/contrib/Hard_Recovery_Guide.md @@ -25,7 +25,7 @@ python manage.py shell ```python # START COPY FIRST PART from django.contrib.auth.models import User -from app.models import Project, Task, ImageUpload +from app.models import Project, Task import os from django.contrib.gis.gdal import GDALRaster from django.contrib.gis.gdal import OGRGeometry @@ -89,17 +89,7 @@ def create_project(project_id, user): project.owner = user project.id = int(project_id) return project -def reindex_shots(projectID, taskID): - project_and_task_path = f'project/{projectID}/task/{taskID}' - try: - with open(f"/webodm/app/media/{project_and_task_path}/assets/images.json", 'r') as file: - camera_shots = json.load(file) - for image_shot in camera_shots: - ImageUpload.objects.update_or_create(task=Task.objects.get(pk=taskID), - image=f"{project_and_task_path}/{image_shot['filename']}") - print(f"Succesfully indexed file {image_shot['filename']}") - except Exception as e: - print(e) + # END COPY FIRST PART ``` @@ -110,7 +100,7 @@ user = User.objects.get(username="YOUR NEW CREATED ADMIN USERNAME HERE") # END COPY COPY SECOND PART ``` -## Step 3. This is the main part of script which make the main magic of the project. It will read media dir and create tasks and projects from the sources, also it will reindex photo sources, if avaliable +## Step 3. This is the main part of script which make the main magic of the project. It will read media dir and create tasks and projects from the sources ```python # START COPY THIRD PART for project_id in os.listdir("/webodm/app/media/project"): @@ -124,7 +114,6 @@ for project_id in os.listdir("/webodm/app/media/project"): task = Task(project=project) task.id = task_id process_task(task) - reindex_shots(project_id, task_id) # END COPY THIRD PART ``` ## Step 4. You must update project ID sequence for new created tasks diff --git a/coreplugins/cloudimport/api_views.py b/coreplugins/cloudimport/api_views.py index b456aaf6..b1fa2dbb 100644 --- a/coreplugins/cloudimport/api_views.py +++ b/coreplugins/cloudimport/api_views.py @@ -4,6 +4,7 @@ import os from os import path from app import models, pending_actions +from app.security import path_traversal_check from app.plugins.views import TaskView from app.plugins.worker import run_function_async from app.plugins import get_current_plugin @@ -105,15 +106,13 @@ def import_files(task_id, files): from app.plugins import logger def download_file(task, file): - path = task.task_path(file['name']) + path = path_traversal_check(task.task_path(file['name']), task.task_path()) download_stream = requests.get(file['url'], stream=True, timeout=60) with open(path, 'wb') as fd: for chunk in download_stream.iter_content(4096): fd.write(chunk) - models.ImageUpload.objects.create(task=task, image=path) - logger.info("Will import {} files".format(len(files))) task = models.Task.objects.get(pk=task_id) task.create_task_directories() @@ -134,4 +133,5 @@ def import_files(task_id, files): task.pending_action = None task.processing_time = 0 task.partial = False + task.images_count = len(task.scan_images()) task.save() diff --git a/coreplugins/dronedb/api_views.py b/coreplugins/dronedb/api_views.py index 1b4323bc..dec635fe 100644 --- a/coreplugins/dronedb/api_views.py +++ b/coreplugins/dronedb/api_views.py @@ -8,10 +8,10 @@ import os from os import listdir, path from app import models, pending_actions +from app.security import path_traversal_check from app.plugins.views import TaskView from app.plugins.worker import run_function_async, task from app.plugins import get_current_plugin -from app.models import ImageUpload from app.plugins import GlobalDataStore, get_site_settings, signals as plugin_signals from coreplugins.dronedb.ddb import DEFAULT_HUB_URL, DroneDB, parse_url, verify_url @@ -218,7 +218,7 @@ def import_files(task_id, carrier): headers['Authorization'] = 'Bearer ' + carrier['token'] def download_file(task, file): - path = task.task_path(file['name']) + path = path_traversal_check(task.task_path(file['name']), task.task_path()) logger.info("Downloading file: " + file['url']) download_stream = requests.get(file['url'], stream=True, timeout=60, headers=headers) @@ -226,8 +226,6 @@ def import_files(task_id, carrier): for chunk in download_stream.iter_content(4096): fd.write(chunk) - models.ImageUpload.objects.create(task=task, image=path) - logger.info("Will import {} files".format(len(files))) task = models.Task.objects.get(pk=task_id) task.create_task_directories() diff --git a/coreplugins/openaerialmap/api.py b/coreplugins/openaerialmap/api.py index 5bf45f66..2b640e6a 100644 --- a/coreplugins/openaerialmap/api.py +++ b/coreplugins/openaerialmap/api.py @@ -9,7 +9,6 @@ from rest_framework import serializers from rest_framework import status from rest_framework.response import Response -from app.models import ImageUpload from app.plugins import GlobalDataStore, get_site_settings, signals as plugin_signals from app.plugins.views import TaskView from app.plugins.worker import task @@ -58,9 +57,10 @@ class Info(TaskView): task_info = get_task_info(task.id) # Populate fields from first image in task - img = ImageUpload.objects.filter(task=task).exclude(image__iendswith='.txt').first() - if img is not None: - img_path = os.path.join(settings.MEDIA_ROOT, img.path()) + imgs = [f for f in task.scan_images() if not f.lower().endswith(".txt")] + if len(imgs) > 0: + img = imgs[0] + img_path = task.get_image_path(img) im = Image.open(img_path) # TODO: for better data we could look over all images From 0dc54a64d793631a905ce862714956e482d3d8da Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 23 Mar 2023 13:32:07 -0400 Subject: [PATCH 34/41] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 39c3efe0..7ccd1218 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "2.0.0", + "version": "2.0.1", "description": "User-friendly, extendable application and API for processing aerial imagery.", "main": "index.js", "scripts": { From 21c0097a05dc7305f8ee3803731be880f9646a00 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 23 Mar 2023 17:13:06 -0400 Subject: [PATCH 35/41] Efficient camera markers --- app/static/app/js/components/Map.jsx | 138 ++--- app/static/app/js/icons/marker-camera.png | Bin 0 -> 5102 bytes app/static/app/js/icons/marker-gcp.png | Bin 0 -> 6109 bytes .../images/markers-matte.png | Bin 14323 -> 0 bytes .../images/markers-matte@2x.png | Bin 31113 -> 0 bytes .../images/markers-plain.png | Bin 7946 -> 0 bytes .../images/markers-shadow.png | Bin 535 -> 0 bytes .../images/markers-shadow@2x.png | Bin 1469 -> 0 bytes .../images/markers-soft.png | Bin 41226 -> 0 bytes .../images/markers-soft@2x.png | Bin 66408 -> 0 bytes .../leaflet/Leaflet.Awesome-markers/index.js | 127 ----- .../leaflet.awesome-markers.css | 124 ----- .../vendor/leaflet/leaflet-markers-canvas.js | 493 ++++++++++++++++++ package.json | 1 + 14 files changed, 566 insertions(+), 317 deletions(-) create mode 100644 app/static/app/js/icons/marker-camera.png create mode 100644 app/static/app/js/icons/marker-gcp.png delete mode 100644 app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-matte.png delete mode 100644 app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-matte@2x.png delete mode 100644 app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-plain.png delete mode 100644 app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-shadow.png delete mode 100644 app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-shadow@2x.png delete mode 100644 app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-soft.png delete mode 100644 app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-soft@2x.png delete mode 100644 app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/index.js delete mode 100644 app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/leaflet.awesome-markers.css create mode 100644 app/static/app/js/vendor/leaflet/leaflet-markers-canvas.js diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index 6604a829..ff17a591 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -26,7 +26,8 @@ import LayersControl from './LayersControl'; import update from 'immutability-helper'; import Utils from '../classes/Utils'; import '../vendor/leaflet/Leaflet.Ajax'; -import '../vendor/leaflet/Leaflet.Awesome-markers'; +import 'rbush'; +import '../vendor/leaflet/leaflet-markers-canvas'; import { _ } from '../classes/gettext'; class Map extends React.Component { @@ -228,41 +229,45 @@ class Map extends React.Component { // Add camera shots layer if available if (meta.task && meta.task.camera_shots && !this.addedCameraShots){ - const shotsLayer = new L.GeoJSON.AJAX(meta.task.camera_shots, { - style: function (feature) { - return { - opacity: 1, - fillOpacity: 0.7, - color: "#000000" - } - }, - pointToLayer: function (feature, latlng) { - return new L.CircleMarker(latlng, { - color: '#3498db', - fillColor: '#3498db', - fillOpacity: 0.9, - radius: 10, - weight: 1 - }); - }, - onEachFeature: function (feature, layer) { - if (feature.properties && feature.properties.filename) { - let root = null; - const lazyrender = () => { - if (!root) root = document.createElement("div"); - ReactDOM.render(, root); - return root; - } - - layer.bindPopup(L.popup( - { - lazyrender, - maxHeight: 450, - minWidth: 320 - })); - } - } + var camIcon = L.icon({ + iconUrl: "/static/app/js/icons/marker-camera.png", + iconSize: [41, 46], + iconAnchor: [17, 46], }); + + const shotsLayer = new L.MarkersCanvas(); + $.getJSON(meta.task.camera_shots) + .done((shots) => { + if (shots.type === 'FeatureCollection'){ + let markers = []; + + shots.features.forEach(s => { + let marker = L.marker( + [s.geometry.coordinates[1], s.geometry.coordinates[0]], + { icon: camIcon } + ); + markers.push(marker); + + if (s.properties && s.properties.filename){ + let root = null; + const lazyrender = () => { + if (!root) root = document.createElement("div"); + ReactDOM.render(, root); + return root; + } + + marker.bindPopup(L.popup( + { + lazyrender, + maxHeight: 450, + minWidth: 320 + })); + } + }); + + shotsLayer.addMarkers(markers, this.map); + } + }); shotsLayer[Symbol.for("meta")] = {name: name + " " + _("(Cameras)"), icon: "fa fa-camera fa-fw"}; this.setState(update(this.state, { @@ -274,44 +279,45 @@ class Map extends React.Component { // Add ground control points layer if available if (meta.task && meta.task.ground_control_points && !this.addedGroundControlPoints){ - const gcpMarker = L.AwesomeMarkers.icon({ - icon: 'dot-circle', - markerColor: 'blue', - prefix: 'fa' + const gcpIcon = L.icon({ + iconUrl: "/static/app/js/icons/marker-gcp.png", + iconSize: [41, 46], + iconAnchor: [17, 46], }); + + const gcpLayer = new L.MarkersCanvas(); + $.getJSON(meta.task.ground_control_points) + .done((gcps) => { + if (gcps.type === 'FeatureCollection'){ + let markers = []; - const gcpLayer = new L.GeoJSON.AJAX(meta.task.ground_control_points, { - style: function (feature) { - return { - opacity: 1, - fillOpacity: 0.7, - color: "#000000" - } - }, - pointToLayer: function (feature, latlng) { - return new L.marker(latlng, { - icon: gcpMarker - }); - }, - onEachFeature: function (feature, layer) { - if (feature.properties && feature.properties.observations) { - // TODO! - let root = null; - const lazyrender = () => { + gcps.features.forEach(gcp => { + let marker = L.marker( + [gcp.geometry.coordinates[1], gcp.geometry.coordinates[0]], + { icon: gcpIcon } + ); + markers.push(marker); + + if (gcp.properties && gcp.properties.observations){ + let root = null; + const lazyrender = () => { if (!root) root = document.createElement("div"); - ReactDOM.render(, root); + ReactDOM.render(, root); return root; - } + } - layer.bindPopup(L.popup( - { - lazyrender, - maxHeight: 450, - minWidth: 320 - })); + marker.bindPopup(L.popup( + { + lazyrender, + maxHeight: 450, + minWidth: 320 + })); } + }); + + gcpLayer.addMarkers(markers, this.map); } - }); + }); gcpLayer[Symbol.for("meta")] = {name: name + " " + _("(GCPs)"), icon: "far fa-dot-circle fa-fw"}; this.setState(update(this.state, { diff --git a/app/static/app/js/icons/marker-camera.png b/app/static/app/js/icons/marker-camera.png new file mode 100644 index 0000000000000000000000000000000000000000..50c90c79fa36be1131b5dde1e104c926e5ef2009 GIT binary patch literal 5102 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 literal 0 HcmV?d00001 diff --git a/app/static/app/js/icons/marker-gcp.png b/app/static/app/js/icons/marker-gcp.png new file mode 100644 index 0000000000000000000000000000000000000000..90613e5bf72e3fcdfb8f15918a24e3ce73db0518 GIT binary patch literal 6109 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 diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-matte@2x.png b/app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-matte@2x.png deleted file mode 100644 index c981244dd3bba2fa7eb186253fdad64d8a2fdc06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31113 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 O1I&%BPZk-vNBs{Ev@NLs diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-shadow.png b/app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-shadow.png deleted file mode 100644 index 33cf95504706aa9d06ad40dbcd2fa168cbd43d13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 535 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 aAw|UvuR9IAZi0KdKr~f#RX!_ONBti%eZQ*! diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-soft@2x.png b/app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-soft@2x.png deleted file mode 100644 index 540ce63759f8a8c7c46d136c14a4392c5802d635..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66408 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"; - }, - - _setIconStyles: function (img, name) { - var options = this.options, - size = L.point(options[name === 'shadow' ? 'shadowSize' : 'iconSize']), - anchor; - - if (name === 'shadow') { - anchor = L.point(options.shadowAnchor || options.iconAnchor); - } else { - anchor = L.point(options.iconAnchor); - } - - if (!anchor && size) { - anchor = size.divideBy(2, true); - } - - img.className = 'awesome-marker-' + name + ' ' + options.className; - - if (anchor) { - img.style.marginLeft = (-anchor.x) + 'px'; - img.style.marginTop = (-anchor.y) + 'px'; - } - - if (size) { - img.style.width = size.x + 'px'; - img.style.height = size.y + 'px'; - } - }, - - createShadow: function () { - var div = document.createElement('div'); - - this._setIconStyles(div, 'shadow'); - return div; - } - }); - - L.AwesomeMarkers.icon = function (options) { - return new L.AwesomeMarkers.Icon(options); - }; - -}(this, document)); - - - diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/leaflet.awesome-markers.css b/app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/leaflet.awesome-markers.css deleted file mode 100644 index 588a99c8..00000000 --- a/app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/leaflet.awesome-markers.css +++ /dev/null @@ -1,124 +0,0 @@ -/* -Author: L. Voogdt -License: MIT -Version: 1.0 -*/ - -/* Marker setup */ -.awesome-marker { - background: url('images/markers-soft.png') no-repeat 0 0; - width: 35px; - height: 46px; - position:absolute; - left:0; - top:0; - display: block; - text-align: center; -} - -.awesome-marker-shadow { - background: url('images/markers-shadow.png') no-repeat 0 0; - width: 36px; - height: 16px; -} - -/* Retina displays */ -@media (min--moz-device-pixel-ratio: 1.5),(-o-min-device-pixel-ratio: 3/2), -(-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5),(min-resolution: 1.5dppx) { - .awesome-marker { - background-image: url('images/markers-soft@2x.png'); - background-size: 720px 46px; - } - .awesome-marker-shadow { - background-image: url('images/markers-shadow@2x.png'); - background-size: 35px 16px; - } -} - -.awesome-marker i { - color: #333; - margin-top: 10px; - display: inline-block; - font-size: 14px; -} - -.awesome-marker .icon-white { - color: #fff; -} - -/* Colors */ -.awesome-marker-icon-red { - background-position: 0 0; -} - -.awesome-marker-icon-darkred { - background-position: -180px 0; -} - -.awesome-marker-icon-lightred { - background-position: -360px 0; -} - -.awesome-marker-icon-orange { - background-position: -36px 0; -} - -.awesome-marker-icon-beige { - background-position: -396px 0; -} - -.awesome-marker-icon-green { - background-position: -72px 0; -} - -.awesome-marker-icon-darkgreen { - background-position: -252px 0; -} - -.awesome-marker-icon-lightgreen { - background-position: -432px 0; -} - -.awesome-marker-icon-blue { - background-position: -108px 0; -} - -.awesome-marker-icon-darkblue { - background-position: -216px 0; -} - -.awesome-marker-icon-lightblue { - background-position: -468px 0; -} - -.awesome-marker-icon-purple { - background-position: -144px 0; -} - -.awesome-marker-icon-darkpurple { - background-position: -288px 0; -} - -.awesome-marker-icon-pink { - background-position: -504px 0; -} - -.awesome-marker-icon-cadetblue { - background-position: -324px 0; -} - -.awesome-marker-icon-white { - background-position: -574px 0; -} - -.awesome-marker-icon-gray { - background-position: -648px 0; -} - -.awesome-marker-icon-lightgray { - background-position: -612px 0; -} - -.awesome-marker-icon-black { - background-position: -682px 0; -} diff --git a/app/static/app/js/vendor/leaflet/leaflet-markers-canvas.js b/app/static/app/js/vendor/leaflet/leaflet-markers-canvas.js new file mode 100644 index 00000000..578f4ce7 --- /dev/null +++ b/app/static/app/js/vendor/leaflet/leaflet-markers-canvas.js @@ -0,0 +1,493 @@ +/*https://github.com/francoisromain/leaflet-markers-canvas/blob/master/licence.md*/ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('leaflet'), require('rbush')) : + typeof define === 'function' && define.amd ? define(['leaflet', 'rbush'], factory) : + (global = global || self, factory(global.L, global.RBush)); + }(this, (function (L, RBush) { 'use strict'; + + L = L && Object.prototype.hasOwnProperty.call(L, 'default') ? L['default'] : L; + RBush = RBush && Object.prototype.hasOwnProperty.call(RBush, 'default') ? RBush['default'] : RBush; + + var markersCanvas = { + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // + // private: properties + // + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + _map: null, + _canvas: null, + _context: null, + + // leaflet markers (used to getBounds) + _markers: [], + + // visible markers + _markersTree: null, + + // every marker positions (even out of the canvas) + _positionsTree: null, + + // icon images index + _icons: {}, + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // + // public: global + // + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + addTo: function addTo(map) { + map.addLayer(this); + + return this; + }, + + getBounds: function getBounds() { + var bounds = new L.LatLngBounds(); + + this._markers.forEach(function (marker) { + bounds.extend(marker.getLatLng()); + }); + + return bounds; + }, + + redraw: function redraw() { + this._redraw(true); + }, + + clear: function clear() { + this._positionsTree = new RBush(); + this._markersTree = new RBush(); + this._markers = []; + this._redraw(true); + }, + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // + // public: markers + // + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + addMarker: function addMarker(marker, map) { + var ref = this._addMarker(marker, map); + var markerBox = ref.markerBox; + var positionBox = ref.positionBox; + var isVisible = ref.isVisible; + + if (markerBox && isVisible) { + this._markersTree.insert(markerBox); + } + + if (positionBox) { + this._positionsTree.insert(positionBox); + } + }, + + // add multiple markers (better for rBush performance) + addMarkers: function addMarkers(markers, map) { + if (!this._markersTree) this._markersTree = new RBush(); + if (!this._positionsTree) this._positionsTree = new RBush(); + + var this$1 = this; + + var markerBoxes = []; + var positionBoxes = []; + + markers.forEach(function (marker) { + var ref = this$1._addMarker(marker, map); + var markerBox = ref.markerBox; + var positionBox = ref.positionBox; + var isVisible = ref.isVisible; + + if (markerBox && isVisible) { + markerBoxes.push(markerBox); + } + + if (positionBox) { + positionBoxes.push(positionBox); + } + }); + + this._markersTree.load(markerBoxes); + this._positionsTree.load(positionBoxes); + }, + + removeMarker: function removeMarker(marker) { + var latLng = marker.getLatLng(); + var isVisible = this._map.getBounds().contains(latLng); + + var positionBox = { + minX: latLng.lng, + minY: latLng.lat, + maxX: latLng.lng, + maxY: latLng.lat, + marker: marker, + }; + + this._positionsTree.remove(positionBox, function (a, b) { + return a.marker._leaflet_id === b.marker._leaflet_id; + }); + + if (isVisible) { + this._redraw(true); + } + }, + + // remove multiple markers (better for rBush performance) + removeMarkers: function removeMarkers(markers) { + var this$1 = this; + + var hasChanged = false; + + markers.forEach(function (marker) { + var latLng = marker.getLatLng(); + var isVisible = this$1._map.getBounds().contains(latLng); + + var positionBox = { + minX: latLng.lng, + minY: latLng.lat, + maxX: latLng.lng, + maxY: latLng.lat, + marker: marker, + }; + + this$1._positionsTree.remove(positionBox, function (a, b) { + return a.marker._leaflet_id === b.marker._leaflet_id; + }); + + if (isVisible) { + hasChanged = true; + } + }); + + if (hasChanged) { + this._redraw(true); + } + }, + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // + // leaflet: default methods + // + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + initialize: function initialize(options) { + L.Util.setOptions(this, options); + }, + + // called by Leaflet on `map.addLayer` + onAdd: function onAdd(map) { + this._map = map; + if (!this._canvas) this._initCanvas(); + this.getPane().appendChild(this._canvas); + + map.on("moveend", this._reset, this); + map.on("resize", this._reset, this); + + map.on("click", this._fire, this); + map.on("mousemove", this._fire, this); + + if (map._zoomAnimated) { + map.on("zoomanim", this._animateZoom, this); + } + + this._reset(); + }, + + // called by Leaflet + onRemove: function onRemove(map) { + this.getPane().removeChild(this._canvas); + + map.off("click", this._fire, this); + map.off("mousemove", this._fire, this); + map.off("moveend", this._reset, this); + map.off("resize", this._reset, this); + + if (map._zoomAnimated) { + map.off("zoomanim", this._animateZoom, this); + } + }, + + setOptions: function setOptions(options) { + L.Util.setOptions(this, options); + + return this.redraw(); + }, + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // + // private: global methods + // + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + _initCanvas: function _initCanvas() { + var ref = this._map.getSize(); + var x = ref.x; + var y = ref.y; + var isAnimated = this._map.options.zoomAnimation && L.Browser.any3d; + + this._canvas = L.DomUtil.create( + "canvas", + "leaflet-markers-canvas-layer leaflet-layer" + ); + this._canvas.width = x; + this._canvas.height = y; + this._context = this._canvas.getContext("2d"); + + L.DomUtil.addClass( + this._canvas, + ("leaflet-zoom-" + (isAnimated ? "animated" : "hide")) + ); + }, + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // + // private: marker methods + // + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + _addMarker: function _addMarker(marker, map) { + if (marker.options.pane !== "markerPane" || !marker.options.icon) { + console.error("This is not a marker", marker); + + return { markerBox: null, positionBox: null, isVisible: null }; + } + + // required for pop-up and tooltip + marker._map = map; + + // add _leaflet_id property + L.Util.stamp(marker); + + var latLng = marker.getLatLng(); + var isVisible = map.getBounds().contains(latLng); + var ref = map.latLngToContainerPoint(latLng); + var x = ref.x; + var y = ref.y; + var ref$1 = marker.options.icon.options; + var iconSize = ref$1.iconSize; + var iconAnchor = ref$1.iconAnchor; + + var markerBox = { + minX: x - iconAnchor[0], + minY: y - iconAnchor[1], + maxX: x + iconSize[0] - iconAnchor[0], + maxY: y + iconSize[1] - iconAnchor[1], + marker: marker, + }; + + var positionBox = { + minX: latLng.lng, + minY: latLng.lat, + maxX: latLng.lng, + maxY: latLng.lat, + marker: marker, + }; + + if (isVisible) { + this._drawMarker(marker, { x: x, y: y }); + } + + this._markers.push(marker); + + return { markerBox: markerBox, positionBox: positionBox, isVisible: isVisible }; + }, + + _drawMarker: function _drawMarker(marker, ref) { + if (!this._map) return; + + var this$1 = this; + var x = ref.x; + var y = ref.y; + + var ref$1 = marker.options.icon.options; + var iconUrl = ref$1.iconUrl; + + if (marker.image) { + this._drawImage(marker, { x: x, y: y }); + } else if (this._icons[iconUrl]) { + marker.image = this._icons[iconUrl].image; + + if (this._icons[iconUrl].isLoaded) { + this._drawImage(marker, { x: x, y: y }); + } else { + this._icons[iconUrl].elements.push({ marker: marker, x: x, y: y }); + } + } else { + var image = new Image(); + image.src = iconUrl; + marker.image = image; + + this._icons[iconUrl] = { + image: image, + isLoaded: false, + elements: [{ marker: marker, x: x, y: y }], + }; + + image.onload = function () { + this$1._icons[iconUrl].isLoaded = true; + this$1._icons[iconUrl].elements.forEach(function (ref) { + var marker = ref.marker; + var x = ref.x; + var y = ref.y; + + this$1._drawImage(marker, { x: x, y: y }); + }); + }; + } + }, + + _drawImage: function _drawImage(marker, ref) { + var x = ref.x; + var y = ref.y; + + var ref$1 = marker.options.icon.options; + var rotationAngle = ref$1.rotationAngle; + var iconAnchor = ref$1.iconAnchor; + var iconSize = ref$1.iconSize; + var angle = rotationAngle || 0; + + this._context.save(); + this._context.translate(x, y); + this._context.rotate((angle * Math.PI) / 180); + this._context.drawImage( + marker.image, + -iconAnchor[0], + -iconAnchor[1], + iconSize[0], + iconSize[1] + ); + this._context.restore(); + }, + + _redraw: function _redraw(clear) { + var this$1 = this; + + if (clear) { + this._context.clearRect(0, 0, this._canvas.width, this._canvas.height); + } + + if (!this._map || !this._positionsTree) { return; } + + var mapBounds = this._map.getBounds(); + var mapBoundsBox = { + minX: mapBounds.getWest(), + minY: mapBounds.getSouth(), + maxX: mapBounds.getEast(), + maxY: mapBounds.getNorth(), + }; + + // draw only visible markers + var markers = []; + this._positionsTree.search(mapBoundsBox).forEach(function (ref) { + var marker = ref.marker; + + var latLng = marker.getLatLng(); + var ref$1 = this$1._map.latLngToContainerPoint(latLng); + var x = ref$1.x; + var y = ref$1.y; + var ref$2 = marker.options.icon.options; + var iconSize = ref$2.iconSize; + var iconAnchor = ref$2.iconAnchor; + + var markerBox = { + minX: x - iconAnchor[0], + minY: y - iconAnchor[1], + maxX: x + iconSize[0] - iconAnchor[0], + maxY: y + iconSize[1] - iconAnchor[1], + marker: marker, + }; + + markers.push(markerBox); + this$1._drawMarker(marker, { x: x, y: y }); + }); + + this._markersTree.clear(); + this._markersTree.load(markers); + }, + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // + // private: event methods + // + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + _reset: function _reset() { + var topLeft = this._map.containerPointToLayerPoint([0, 0]); + L.DomUtil.setPosition(this._canvas, topLeft); + + var ref = this._map.getSize(); + var x = ref.x; + var y = ref.y; + this._canvas.width = x; + this._canvas.height = y; + + this._redraw(); + }, + + _fire: function _fire(event) { + if (!this._markersTree) { return; } + + var ref = event.containerPoint; + var x = ref.x; + var y = ref.y; + var markers = this._markersTree.search({ + minX: x, + minY: y, + maxX: x, + maxY: y, + }); + + if (markers && markers.length) { + this._map._container.style.cursor = "pointer"; + var marker = markers[0].marker; + + if (event.type === "click") { + if (marker.listens("click")) { + marker.fire("click"); + } + } + + if (event.type === "mousemove") { + if (this._mouseOverMarker && this._mouseOverMarker !== marker) { + if (this._mouseOverMarker.listens("mouseout")) { + this._mouseOverMarker.fire("mouseout"); + } + } + + if (!this._mouseOverMarker || this._mouseOverMarker !== marker) { + this._mouseOverMarker = marker; + if (marker.listens("mouseover")) { + marker.fire("mouseover"); + } + } + } + } else { + this._map._container.style.cursor = ""; + if (event.type === "mousemove" && this._mouseOverMarker) { + if (this._mouseOverMarker.listens("mouseout")) { + this._mouseOverMarker.fire("mouseout"); + } + + delete this._mouseOverMarker; + } + } + }, + + _animateZoom: function _animateZoom(event) { + var scale = this._map.getZoomScale(event.zoom); + var offset = this._map._latLngBoundsToNewLayerBounds( + this._map.getBounds(), + event.zoom, + event.center + ).min; + + L.DomUtil.setTransform(this._canvas, offset, scale); + }, + }; + + L.MarkersCanvas = L.Layer.extend(markersCanvas); + + }))); \ No newline at end of file diff --git a/package.json b/package.json index 7ccd1218..6d9f5644 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "proj4": "^2.4.3", "qrcode.react": "^0.7.2", "raw-loader": "^0.5.1", + "rbush": "^3.0.1", "react": "^16.4.0", "react-dom": "^16.4.0", "react-router": "^4.1.1", From 8724acf794481b3e0c06e2eee2ab1f690df397aa Mon Sep 17 00:00:00 2001 From: DanV Date: Fri, 24 Mar 2023 13:02:21 +0100 Subject: [PATCH 36/41] Update task.py copyfileobj does not exist, I presume it should be shutil.copyfileobj Rebuilding WebODM on a machine in use now to test if this fixes it. Currently this breaks uploads --- app/models/task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/task.py b/app/models/task.py index 471b56a5..23426d09 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1135,4 +1135,4 @@ class Task(models.Model): fd.write(chunk) else: with open(file.temporary_file_path(), 'rb') as f: - copyfileobj(f, fd) \ No newline at end of file + shutil.copyfileobj(f, fd) From 00ebfeb550dfe9adaafecc7d6299e81631ec66c9 Mon Sep 17 00:00:00 2001 From: HeDo Date: Mon, 27 Mar 2023 16:07:59 +0200 Subject: [PATCH 37/41] Fixed imports --- coreplugins/dronedb/api_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coreplugins/dronedb/api_views.py b/coreplugins/dronedb/api_views.py index dec635fe..1b7bec6b 100644 --- a/coreplugins/dronedb/api_views.py +++ b/coreplugins/dronedb/api_views.py @@ -208,10 +208,10 @@ def import_files(task_id, carrier): import requests from app import models from app.plugins import logger + from app.security import path_traversal_check files = carrier['files'] - #headers = CaseInsensitiveDict() headers = {} if carrier['token'] != None: From 020596609392c1a0667811484a7d508f745ddd11 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 30 Mar 2023 10:11:13 -0400 Subject: [PATCH 38/41] Docker compose support --- start.sh | 1 - webodm.sh | 63 +++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/start.sh b/start.sh index f36e4796..a99d1e1a 100755 --- a/start.sh +++ b/start.sh @@ -114,7 +114,6 @@ congrats(){ echo -e "\033[93m" echo Open a web browser and navigate to $proto://$WO_HOST:$WO_PORT echo -e "\033[39m" - echo -e "\033[91mNOTE:\033[39m Windows users using docker should replace localhost with the IP of their docker machine's IP. To find what that is, run: docker-machine ip") & } if [ "$1" = "--setup-devenv" ] || [ "$2" = "--setup-devenv" ] || [ "$1" = "--no-gunicorn" ]; then diff --git a/webodm.sh b/webodm.sh index e0be8926..524f8f82 100755 --- a/webodm.sh +++ b/webodm.sh @@ -234,11 +234,49 @@ if [[ $gpu = true ]]; then prepare_intel_render_group fi -# $1 = command | $2 = help_text | $3 = install_command (optional) +docker_compose="docker-compose" +check_docker_compose(){ + dc_msg_ok="\033[92m\033[1m OK\033[0m\033[39m" + + # Check if docker-compose exists + hash "docker-compose" 2>/dev/null || not_found=true + if [[ $not_found ]]; then + # Check if compose plugin is installed + if ! docker compose > /dev/null 2>&1; then + + if [ "${platform}" = "Linux" ] && [ -z "$1" ] && [ ! -z "$HOME" ]; then + echo -e "Checking for docker compose... \033[93mnot found, we'll attempt to install it\033[39m" + check_command "curl" "Cannot automatically install docker compose. Please visit https://docs.docker.com/compose/install/" "" "silent" + DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker} + mkdir -p $DOCKER_CONFIG/cli-plugins + curl -SL# https://github.com/docker/compose/releases/download/v2.17.2/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose + chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose + check_docker_compose "y" + else + if [ -z "$1" ]; then + echo -e "Checking for docker compose... \033[93mnot found, please visit https://docs.docker.com/compose/install/ to install docker compose\033[39m" + else + echo -e "\033[93mCannot automatically install docker compose. Please visit https://docs.docker.com/compose/install/\033[39m" + fi + return 1 + fi + else + docker_compose="docker compose" + fi + else + docker_compose="docker-compose" + fi + + if [ -z "$1" ]; then + echo -e "Checking for $docker_compose... $dc_msg_ok" + fi +} + +# $1 = command | $2 = help_text | $3 = install_command (optional) | $4 = silent check_command(){ check_msg_prefix="Checking for $1... " check_msg_result="\033[92m\033[1m OK\033[0m\033[39m" - + unset not_found hash "$1" 2>/dev/null || not_found=true if [[ $not_found ]]; then @@ -254,7 +292,10 @@ check_command(){ fi fi - echo -e "$check_msg_prefix $check_msg_result" + if [ -z "$4" ]; then + echo -e "$check_msg_prefix $check_msg_result" + fi + if [[ $not_found ]]; then return 1 fi @@ -262,7 +303,7 @@ check_command(){ environment_check(){ check_command "docker" "https://www.docker.com/" - check_command "docker-compose" "Run \033[1mpip install docker-compose\033[0m" "pip install docker-compose" + check_docker_compose } run(){ @@ -293,7 +334,7 @@ start(){ echo "Make sure to issue a $0 down if you decide to change the environment." echo "" - command="docker-compose -f docker-compose.yml" + command="$docker_compose -f docker-compose.yml" if [[ $WO_DEFAULT_NODES -gt 0 ]]; then if [ "${GPU_NVIDIA}" = true ]; then @@ -365,7 +406,7 @@ start(){ } down(){ - command="docker-compose -f docker-compose.yml" + command="$docker_compose -f docker-compose.yml" if [ "${GPU_NVIDIA}" = true ]; then command+=" -f docker-compose.nodeodm.gpu.nvidia.yml" @@ -381,10 +422,10 @@ down(){ } rebuild(){ - run "docker-compose down --remove-orphans" + run "$docker_compose down --remove-orphans" run "rm -fr node_modules/ || sudo rm -fr node_modules/" run "rm -fr nodeodm/external/NodeODM || sudo rm -fr nodeodm/external/NodeODM" - run "docker-compose -f docker-compose.yml -f docker-compose.build.yml build --no-cache" + run "$docker_compose -f docker-compose.yml -f docker-compose.build.yml build --no-cache" #run "docker images --no-trunc -aqf \"dangling=true\" | xargs docker rmi" echo -e "\033[1mDone!\033[0m You can now start WebODM by running $0 start" } @@ -403,7 +444,7 @@ run_tests(){ echo -e "\033[1mDone!\033[0m Everything looks in order." else echo "Running tests in webapp container" - run "docker-compose exec webapp /bin/bash -c \"/webodm/webodm.sh test\"" + run "$docker_compose exec webapp /bin/bash -c \"/webodm/webodm.sh test\"" fi } @@ -434,7 +475,7 @@ elif [[ $1 = "stop" ]]; then environment_check echo "Stopping WebODM..." - command="docker-compose -f docker-compose.yml" + command="$docker_compose -f docker-compose.yml" if [ "${GPU_NVIDIA}" = true ]; then command+=" -f docker-compose.nodeodm.gpu.nvidia.yml" @@ -474,7 +515,7 @@ elif [[ $1 = "update" ]]; then fi fi - command="docker-compose -f docker-compose.yml" + command="$docker_compose -f docker-compose.yml" if [[ $WO_DEFAULT_NODES -gt 0 ]]; then if [ "${GPU_NVIDIA}" = true ]; then From fca64b7d09e66fcb5e763b4e6e670edef96485fa Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 30 Mar 2023 10:22:27 -0400 Subject: [PATCH 39/41] Add env check --- webodm.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/webodm.sh b/webodm.sh index 524f8f82..180526f2 100755 --- a/webodm.sh +++ b/webodm.sh @@ -501,6 +501,7 @@ elif [[ $1 = "rebuild" ]]; then echo "Rebuilding WebODM..." rebuild elif [[ $1 = "update" ]]; then + environment_check down echo "Updating WebODM..." From 31e1770ce790c1ec6ce603bc17eca897ed442314 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 30 Mar 2023 10:48:08 -0400 Subject: [PATCH 40/41] Add swap --- .github/workflows/test-docker.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index eff6441f..7780d000 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -11,7 +11,10 @@ jobs: with: submodules: 'recursive' name: Checkout - + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 - name: Build and Test run: | docker-compose -f docker-compose.yml -f docker-compose.build.yml build --build-arg TEST_BUILD=ON From fd80f494f256e47ffa9eb434101931d4aa81748c Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 30 Mar 2023 16:31:38 -0400 Subject: [PATCH 41/41] Fix start.sh --- start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.sh b/start.sh index a99d1e1a..4c8763d2 100755 --- a/start.sh +++ b/start.sh @@ -113,7 +113,7 @@ congrats(){ echo -e "\033[93m" echo Open a web browser and navigate to $proto://$WO_HOST:$WO_PORT - echo -e "\033[39m" + echo -e "\033[39m") & } if [ "$1" = "--setup-devenv" ] || [ "$2" = "--setup-devenv" ] || [ "$1" = "--no-gunicorn" ]; then