PoC alignment UI

pull/1615/head
Piero Toffanin 2025-02-18 18:52:43 +01:00
rodzic a1681c94d5
commit 5c26ff0742
5 zmienionych plików z 125 dodań i 7 usunięć

Wyświetl plik

@ -91,6 +91,7 @@ class TaskViewSet(viewsets.ViewSet):
parser_classes = (parsers.MultiPartParser, parsers.JSONParser, parsers.FormParser, )
ordering_fields = '__all__'
filter_fields = ('status', ) # TODO: add filter fields
def get_permissions(self):
"""
@ -238,11 +239,25 @@ class TaskViewSet(viewsets.ViewSet):
def create(self, request, project_pk=None):
project = get_and_check_project(request, project_pk, ('change_project', ))
# Check if an alignment field is set to a valid task
# this means a user wants to align this task with another
align_to = request.data.get('align_to')
align_task = None
if align_to is not None and align_to != "auto" and align_to != "":
try:
align_task = models.Task.objects.get(pk=align_to)
get_and_check_project(request, align_task.project.id, ('view_project', ))
except ObjectDoesNotExist:
raise exceptions.ValidationError(detail=_("Cannot create task, alignment task is not valid"))
# If this is a partial task, we're going to upload images later
# for now we just create a placeholder task.
if request.data.get('partial'):
task = models.Task.objects.create(project=project,
pending_action=pending_actions.RESIZE if 'resize_to' in request.data else None)
if align_task is not None:
task.set_alignment_file_from(align_task)
serializer = TaskSerializer(task, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
@ -255,7 +270,8 @@ class TaskViewSet(viewsets.ViewSet):
with transaction.atomic():
task = models.Task.objects.create(project=project,
pending_action=pending_actions.RESIZE if 'resize_to' in request.data else None)
if align_task is not None:
task.set_alignment_file_from(align_task)
task.handle_images_upload(files)
task.images_count = len(task.scan_images())

Wyświetl plik

@ -1241,7 +1241,26 @@ class Task(models.Model):
def get_image_path(self, filename):
p = self.task_path(filename)
return path_traversal_check(p, self.task_path())
def set_alignment_file_from(self, align_task):
tp = self.task_path()
if not os.path.exists(tp):
os.makedirs(tp, exist_ok=True)
alignment_file = align_task.assets_path(self.ASSETS_MAP['georeferenced_model.laz'])
dst_file = self.task_path("align.laz")
if os.path.exists(dst_file):
os.unlink(dst_file)
if os.path.exists(alignment_file):
try:
os.link(alignment_file, dst_file)
except:
shutil.copy(alignment_file, dst_file)
else:
logger.warn("Cannot set alignment file for {}, {} does not exist".format(self, alignment_file))
def handle_images_upload(self, files):
uploaded = {}
for file in files:

Wyświetl plik

@ -24,12 +24,14 @@ const Colors = {
class MapPreview extends React.Component {
static defaultProps = {
getFiles: null,
onPolygonChange: () => {}
onPolygonChange: () => {},
onImagesBboxChanged: () => {}
};
static propTypes = {
getFiles: PropTypes.func.isRequired,
onPolygonChange: PropTypes.func
onPolygonChange: PropTypes.func,
onImagesBboxChanged: PropTypes.func
};
constructor(props) {
@ -214,6 +216,8 @@ _('Example:'),
this.map.fitBounds(this.imagesGroup.getBounds());
}
this.props.onImagesBboxChanged(this.computeBbox(this.exifData));
this.setState({showLoading: false});
}).catch(e => {
@ -221,6 +225,20 @@ _('Example:'),
});
}
computeBbox = exifData => {
// minx, maxx, miny, maxy
let bbox = [Infinity, -Infinity, Infinity, -Infinity];
exifData.forEach(ed => {
if (ed.gps){
bbox[0] = Math.min(bbox[0], ed.gps.longitude);
bbox[1] = Math.max(bbox[1], ed.gps.longitude);
bbox[2] = Math.min(bbox[2], ed.gps.latitude);
bbox[3] = Math.max(bbox[3], ed.gps.latitude);
}
});
return bbox;
}
readExifData = () => {
return new Promise((resolve, reject) => {
const files = this.props.getFiles();

Wyświetl plik

@ -7,12 +7,15 @@ import ResizeModes from '../classes/ResizeModes';
import MapPreview from './MapPreview';
import update from 'immutability-helper';
import PluginsAPI from '../classes/plugins/API';
import statusCodes from '../classes/StatusCodes';
import { _, interpolate } from '../classes/gettext';
class NewTaskPanel extends React.Component {
static defaultProps = {
filesCount: 0,
showResize: false
showResize: false,
showAlign: false,
projectId: null
};
static propTypes = {
@ -20,7 +23,9 @@ class NewTaskPanel extends React.Component {
onCancel: PropTypes.func,
filesCount: PropTypes.number,
showResize: PropTypes.bool,
showAlign: PropTypes.bool,
getFiles: PropTypes.func,
projectId: PropTypes.number,
suggestedTaskName: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
};
@ -31,6 +36,9 @@ class NewTaskPanel extends React.Component {
editTaskFormLoaded: false,
resizeMode: Storage.getItem('resize_mode') === null ? ResizeModes.YES : ResizeModes.fromString(Storage.getItem('resize_mode')),
resizeSize: parseInt(Storage.getItem('resize_size')) || 2048,
alignTo: "auto",
alignTasks: [], // loaded on mount if showAlign is true
loadingAlignTasks: false,
items: [], // Coming from plugins,
taskInfo: {},
inReview: false,
@ -63,6 +71,26 @@ class NewTaskPanel extends React.Component {
});
}
componentWillUnmount(){
if (this.alignTasksRequest) this.alignTasksRequest.abort();
}
loadAlignTasks = (bbox) => {
// TODO: filter by status on server
this.setState({alignTasks: [], alignTo: "auto", loadingAlignTasks: true});
this.alignTasksRequest =
$.getJSON(`/api/projects/${this.props.projectId}/tasks/?ordering=-created_at`, tasks => {
if (Array.isArray(tasks)){
this.setState({loadingAlignTasks: false, alignTasks: tasks.filter(t => t.status === statusCodes.COMPLETED && t.available_assets.indexOf("georeferenced_model.laz") !== -1)});
}else{
this.setState({loadingAlignTasks: false});
}
}).fail(() => {
this.setState({loadingAlignTasks: false});
});
}
save(e){
if (!this.state.inReview){
this.setState({inReview: true});
@ -99,7 +127,8 @@ class NewTaskPanel extends React.Component {
getTaskInfo(){
return Object.assign(this.taskForm.getTaskInfo(), {
resizeSize: this.state.resizeSize,
resizeMode: this.state.resizeMode
resizeMode: this.state.resizeMode,
alignTo: this.state.alignTo
});
}
@ -148,6 +177,21 @@ class NewTaskPanel extends React.Component {
if (this.taskForm) this.taskForm.forceUpdate();
}
handleImagesBboxChange = (bbox) => {
if (this.props.showAlign){
console.log("TODO! Load alignment tasks that fit within", bbox);
this.loadAlignTasks(bbox);
}
}
handleAlignToChanged = e => {
this.setState({alignTo: e.target.value});
setTimeout(() => {
this.handleFormChanged();
}, 0);
}
render() {
let filesCountOk = true;
if (this.taskForm && !this.taskForm.checkFilesCount(this.props.filesCount)) filesCountOk = false;
@ -176,6 +220,7 @@ class NewTaskPanel extends React.Component {
{this.state.showMapPreview ? <MapPreview
getFiles={this.props.getFiles}
onPolygonChange={this.handlePolygonChange}
onImagesBboxChanged={this.handleImagesBboxChange}
ref={(domNode) => {this.mapPreview = domNode; }}
/> : ""}
@ -189,6 +234,22 @@ class NewTaskPanel extends React.Component {
ref={(domNode) => { if (domNode) this.taskForm = domNode; }}
/>
{this.state.editTaskFormLoaded && this.props.showAlign && this.state.showMapPreview ?
<div>
<div className="form-group">
<label className="col-sm-2 control-label">{_("Alignment")}</label>
<div className="col-sm-10">
<select className="form-control" disabled={this.state.loadingAlignTasks} value={this.state.alignTo} onChange={this.handleAlignToChanged}>
<option value="auto" key="auto">{this.state.loadingAlignTasks ? _("Loading...") : _("Automatic")}</option>
{this.state.alignTasks.map(t =>
<option value={t.id} key={t.id}>{t.name}</option>
)}
</select>
</div>
</div>
</div>
: ""}
{this.state.editTaskFormLoaded && this.props.showResize ?
<div>
<div className="form-group">
@ -228,6 +289,7 @@ class NewTaskPanel extends React.Component {
</div>)}
</div>
: ""}
</div>
{this.state.editTaskFormLoaded ?

Wyświetl plik

@ -418,7 +418,8 @@ class ProjectListItem extends React.Component {
options: taskInfo.options,
processing_node: taskInfo.selectedNode.id,
auto_processing_node: taskInfo.selectedNode.key == "auto",
partial: true
partial: true,
align_to: taskInfo.alignTo
};
if (taskInfo.resizeMode === ResizeModes.YES){
@ -780,6 +781,8 @@ class ProjectListItem extends React.Component {
suggestedTaskName={this.handleTaskTitleHint}
filesCount={this.state.upload.totalCount}
showResize={true}
showAlign={numTasks > 0}
projectId={this.state.data.id}
getFiles={() => this.state.upload.files }
/>
: ""}