Merge pull request #1 from OpenDroneMap/master

Updating to WebODM
pull/1321/head
Tariq Islam 2023-03-31 01:04:24 -05:00 zatwierdzone przez GitHub
commit 90acb3dc41
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
73 zmienionych plików z 2140 dodań i 675 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -15,7 +15,7 @@ RUN printf "deb http://old-releases.ubuntu.com/ubuntu/ hirsute main restricted\n
# Install Node.js
RUN apt-get -qq update && apt-get -qq install -y --no-install-recommends wget curl && \
wget --no-check-certificate https://deb.nodesource.com/setup_12.x -O /tmp/node.sh && bash /tmp/node.sh && \
wget --no-check-certificate https://deb.nodesource.com/setup_14.x -O /tmp/node.sh && bash /tmp/node.sh && \
apt-get -qq update && apt-get -qq install -y nodejs && \
# Install Python3, GDAL, PDAL, nginx, letsencrypt, psql
apt-get -qq update && apt-get -qq install -y --no-install-recommends python3 python3-pip python3-setuptools python3-wheel git g++ python3-dev python2.7-dev libpq-dev binutils libproj-dev gdal-bin pdal libgdal-dev python3-gdal nginx certbot grass-core gettext-base cron postgresql-client-13 gettext tzdata && \

Wyświetl plik

@ -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)

Wyświetl plik

@ -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()

Wyświetl plik

@ -1,13 +1,19 @@
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
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.contrib.postgres.search import SearchQuery, SearchVector
from django.contrib.postgres.aggregates import StringAgg
from django.db.models import Q
from app import models
from .tasks import TaskIDsSerializer
from .tags import TagsField, parse_tags_input
from .common import get_and_check_project
from django.utils.translation import gettext as _
@ -19,8 +25,10 @@ class ProjectSerializer(serializers.ModelSerializer):
owner = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
owned = serializers.SerializerMethodField()
created_at = serializers.ReadOnlyField()
permissions = serializers.SerializerMethodField()
tags = TagsField(required=False)
def get_permissions(self, obj):
if 'request' in self.context:
@ -28,12 +36,61 @@ class ProjectSerializer(serializers.ModelSerializer):
else:
# Cannot list permissions, no user is associated with request (happens when serializing ui test mocks)
return []
def get_owned(self, obj):
if 'request' in self.context:
user = self.context['request'].user
return user.is_superuser or obj.owner.id == user.id
return False
class Meta:
model = models.Project
exclude = ('deleting', )
class ProjectFilter(filters.FilterSet):
search = filters.CharFilter(method='filter_search')
def filter_search(self, qs, name, value):
value = value.replace(":", "#")
tag_pattern = re.compile("#[^\s]+")
tags = set(re.findall(tag_pattern, value))
task_tags = set([t for t in tags if t.startswith("##")])
project_tags = tags - task_tags
task_tags = [t.replace("##", "") for t in task_tags]
project_tags = [t.replace("#", "") for t in project_tags]
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(task_tags) > 0:
task_tags_vec = SearchVector("task__tags")
tags_query = SearchQuery(task_tags[0])
for t in task_tags[1:]:
tags_query = tags_query & SearchQuery(t)
qs = qs.annotate(tt_search=task_tags_vec).filter(tt_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()
class Meta:
model = models.Project
fields = ['search', 'id', 'name', 'description', 'created_at']
class ProjectViewSet(viewsets.ModelViewSet):
"""
Project get/add/delete/update
@ -45,6 +102,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
@ -52,7 +110,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
if self.paginator and self.request.query_params.get(self.paginator.page_query_param, None) is None:
return None
return super().paginate_queryset(queryset)
@action(detail=True, methods=['post'])
def duplicate(self, request, pk=None):
"""
@ -60,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:
@ -89,6 +147,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
with transaction.atomic():
project.name = request.data.get('name', '')
project.description = request.data.get('description', '')
project.tags = TagsField().to_internal_value(parse_tags_input(request.data.get('tags', [])))
project.save()
form_perms = request.data.get('permissions')
@ -141,7 +200,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
project = get_and_check_project(request, pk, ('delete_project', ))
# Owner? Delete the project
if project.owner == request.user:
if project.owner == request.user or request.user.is_superuser:
return super().destroy(self, request, pk=pk)
else:
# Do not remove the project, simply remove all user's permissions to the project

27
app/api/tags.py 100644
Wyświetl plik

@ -0,0 +1,27 @@
from rest_framework import serializers
import json
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])
def parse_tags_input(tags):
if tags is None:
return []
if isinstance(tags, str):
try:
r = json.loads(tags)
if isinstance(r, list):
return r
else:
raise Exception("Invalid tags string")
except:
return []
elif isinstance(tags, list):
return list(map(str, tags))
else:
return []

Wyświetl plik

@ -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(required=False)
def get_processing_node_name(self, obj):
if obj.processing_node is not None:
@ -177,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"))
@ -204,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)
@ -254,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)

Wyświetl plik

@ -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):

Wyświetl plik

@ -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):

Wyświetl plik

@ -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/<NEW_TASK_ID>/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),
]

Wyświetl plik

@ -9,7 +9,7 @@ from webodm import settings
class Migration(migrations.Migration):
dependencies = [
('app', '0015_public_task_uuids'),
('app', '0014_public_task_uuids'),
]
operations = [

Wyświetl plik

@ -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()

Wyświetl plik

@ -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),
),
]

Wyświetl plik

@ -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',

Wyświetl plik

@ -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'),
),
]

Wyświetl plik

@ -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',
),
]

Wyświetl plik

@ -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")

Wyświetl plik

@ -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")

Wyświetl plik

@ -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:
@ -53,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()

Wyświetl plik

@ -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
@ -276,7 +277,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")
@ -309,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,
@ -429,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
@ -628,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)
@ -1121,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:
shutil.copyfileobj(f, fd)

Wyświetl plik

@ -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");
@ -188,7 +191,7 @@ footer,
border-bottom-color: theme("border");
}
.theme-border{
border-color: theme("border");
border-color: theme("border") !important;
}
/* Highlight */
@ -261,4 +264,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");
}
}

Wyświetl plik

@ -28,10 +28,12 @@ class Dashboard extends React.Component {
return $.ajax({
url: `/api/projects/`,
type: 'POST',
data: {
contentType: 'application/json',
data: JSON.stringify({
name: project.name,
description: project.descr
}
description: project.descr,
tags: project.tags
})
}).done(() => {
this.projectList.refresh();
});
@ -39,13 +41,15 @@ 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 <ProjectList
source={`/api/projects/?ordering=-created_at&page=${page}`}
source={`/api/projects/${Utils.toSearchQuery(q)}`}
ref={(domNode) => { this.projectList = domNode; }}
currentPage={page}
currentPage={q.page}
currentSearch={q.search}
history={history}
/>;
};

Wyświetl plik

@ -0,0 +1,21 @@
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){
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");
}
}

Wyświetl plik

@ -1,8 +1,10 @@
import React from 'react';
import '../css/EditProjectDialog.scss';
import FormDialog from './FormDialog';
import PropTypes from 'prop-types';
import ErrorMessage from './ErrorMessage';
import EditPermissionsPanel from './EditPermissionsPanel';
import TagsField from './TagsField';
import { _ } from '../classes/gettext';
class EditProjectDialog extends React.Component {
@ -10,11 +12,12 @@ class EditProjectDialog extends React.Component {
projectName: "",
projectDescr: "",
projectId: -1,
projectTags: [],
title: _("New Project"),
saveLabel: _("Create Project"),
savingLabel: _("Creating project..."),
saveIcon: "glyphicon glyphicon-plus",
deleteWarning: _("All tasks, images and models associated with this project will be permanently deleted. Are you sure you want to continue?"),
deleteWarning: "",
show: false,
showDuplicate: false,
showPermissions: false,
@ -25,6 +28,7 @@ class EditProjectDialog extends React.Component {
projectName: PropTypes.string,
projectDescr: PropTypes.string,
projectId: PropTypes.number,
projectTags: PropTypes.array,
saveAction: PropTypes.func.isRequired,
onShow: PropTypes.func,
deleteAction: PropTypes.func,
@ -46,7 +50,9 @@ class EditProjectDialog extends React.Component {
name: props.projectName,
descr: props.projectDescr !== null ? props.projectDescr : "",
duplicating: false,
error: ""
tags: props.projectTags,
error: "",
showTagsField: !!props.projectTags.length
};
this.reset = this.reset.bind(this);
@ -60,6 +66,8 @@ class EditProjectDialog extends React.Component {
name: this.props.projectName,
descr: this.props.projectDescr,
duplicating: false,
tags: this.props.projectTags,
showTagsField: !!this.props.projectTags.length,
error: ""
});
}
@ -68,6 +76,7 @@ class EditProjectDialog extends React.Component {
const res = {
name: this.state.name,
descr: this.state.descr,
tags: this.state.tags
};
if (this.editPermissionsPanel){
@ -128,7 +137,26 @@ class EditProjectDialog extends React.Component {
});
}
toggleTagsField = () => {
if (!this.state.showTagsField){
setTimeout(() => {
if (this.tagsField) this.tagsField.focus();
}, 0);
}
this.setState({showTagsField: !this.state.showTagsField});
}
render(){
let tagsField = "";
if (this.state.showTagsField){
tagsField = (<div className="form-group">
<label className="col-sm-2 control-label">{_("Tags")}</label>
<div className="col-sm-10">
<TagsField onUpdate={(tags) => this.state.tags = tags } tags={this.state.tags} ref={domNode => this.tagsField = domNode}/>
</div>
</div>);
}
return (
<FormDialog {...this.props}
getFormData={this.getFormData}
@ -137,12 +165,16 @@ class EditProjectDialog extends React.Component {
leftButtons={this.props.showDuplicate ? [<button key="duplicate" disabled={this.duplicating} onClick={this.handleDuplicate} className="btn btn-default"><i className={"fa " + (this.state.duplicating ? "fa-circle-notch fa-spin fa-fw" : "fa-copy")}></i> Duplicate</button>] : undefined}
ref={(domNode) => { this.dialog = domNode; }}>
<ErrorMessage bind={[this, "error"]} />
<div className="form-group">
<div className="form-group edit-project-dialog">
<label className="col-sm-2 control-label">{_("Name")}</label>
<div className="col-sm-10">
<div className="col-sm-10 name-fields">
<input type="text" className="form-control" ref={(domNode) => { this.nameInput = domNode; }} value={this.state.name} onChange={this.handleChange('name')} onKeyPress={e => this.dialog.handleEnter(e)} />
<button type="button" title={_("Add tags")} onClick={this.toggleTagsField} className="btn btn-sm btn-secondary toggle-tags">
<i className="fa fa-tag"></i>
</button>
</div>
</div>
{tagsField}
<div className="form-group">
<label className="col-sm-2 control-label">{_("Description (optional)")}</label>
<div className="col-sm-10">

Wyświetl plik

@ -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';
@ -45,10 +46,13 @@ class EditTaskForm extends React.Component {
processingNodes: [],
selectedPreset: null,
presets: [],
tags: props.task !== null ? Utils.clone(props.task.tags) : [],
editingPreset: false,
loadingTaskName: false
loadingTaskName: false,
showTagsField: props.task !== null ? !!props.task.tags.length : false
};
this.handleNameChange = this.handleNameChange.bind(this);
@ -354,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,
name: name !== "" ? name : this.state.namePlaceholder,
selectedNode: selectedNode,
options: this.getAvailableOptionsOnly(selectedPreset.options, selectedNode.options)
options: this.getAvailableOptionsOnly(selectedPreset.options, selectedNode.options),
tags
};
}
@ -485,6 +490,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 (<div className="edit-task-panel">
@ -513,10 +527,10 @@ class EditTaskForm extends React.Component {
{!this.state.presetActionPerforming ?
<div className="btn-group presets-dropdown">
<button type="button" className="btn btn-default" title={_("Edit Task Options")} onClick={this.handleEditPreset}>
<button type="button" className="btn btn-sm btn-default" title={_("Edit Task Options")} onClick={this.handleEditPreset}>
<i className="fa fa-sliders-h"></i> {_("Edit")}
</button>
<button type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown">
<button type="button" className="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
<span className="caret"></span>
</button>
<ul className="dropdown-menu">
@ -543,8 +557,19 @@ class EditTaskForm extends React.Component {
<ErrorMessage className="preset-error" bind={[this, 'presetError']} />
</div>);
let tagsField = "";
if (this.state.showTagsField){
tagsField = (<div className="form-group">
<label className="col-sm-2 control-label">{_("Tags")}</label>
<div className="col-sm-10">
<TagsField onUpdate={(tags) => this.state.tags = tags } tags={this.state.tags} ref={domNode => this.tagsField = domNode}/>
</div>
</div>);
}
taskOptions = (
<div>
{tagsField}
<div className="form-group">
<label className="col-sm-2 control-label">{_("Processing Node")}</label>
<div className="col-sm-10">
@ -588,7 +613,7 @@ class EditTaskForm extends React.Component {
<div className="edit-task-form">
<div className="form-group">
<label className="col-sm-2 control-label">{_("Name")}</label>
<div className="col-sm-10">
<div className="col-sm-10 name-fields">
{this.state.loadingTaskName ?
<i className="fa fa-circle-notch fa-spin fa-fw name-loading"></i>
: ""}
@ -596,8 +621,12 @@ class EditTaskForm extends React.Component {
onChange={this.handleNameChange}
className="form-control"
placeholder={this.state.namePlaceholder}
value={this.state.name}
value={this.state.name}
/>
<button type="button" title={_("Add tags")} onClick={this.toggleTagsField} className="btn btn-sm btn-secondary toggle-tags">
<i className="fa fa-tag"></i>
</button>
</div>
</div>
{taskOptions}

Wyświetl plik

@ -144,7 +144,7 @@ class FormDialog extends React.Component {
handleDelete(){
if (this.props.deleteAction){
if (this.props.deleteWarning === false || window.confirm(this.props.deleteWarning)){
if (!this.props.deleteWarning || window.confirm(this.props.deleteWarning)){
this.setState({deleting: true});
this.props.deleteAction()
.fail(e => {

Wyświetl plik

@ -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(<ImagePopup task={meta.task} feature={feature}/>, 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(<ImagePopup task={meta.task} feature={s}/>, 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(<GCPPopup task={meta.task} feature={feature}/>, root);
ReactDOM.render(<GCPPopup task={meta.task} feature={gcp}/>, 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, {

Wyświetl plik

@ -1,45 +1,182 @@
import React from 'react';
import { Link } from 'react-router-dom';
import '../css/Paginator.scss';
import { Link, withRouter } from 'react-router-dom';
import SortPanel from './SortPanel';
import Utils from '../classes/Utils';
import { _ } from '../classes/gettext';
let decodeSearch = (search) => {
return window.decodeURI(search.replace(/:/g, "#"));
};
class Paginator extends React.Component {
constructor(props){
super(props);
const q = Utils.queryParams(props.location);
this.state = {
searchText: decodeSearch(q.search || ""),
sortKey: q.ordering || "-created_at"
}
this.sortItems = [{
key: "created_at",
label: _("Created on")
},{
key: "name",
label: _("Name")
},{
key: "tags",
label: _("Tags")
},{
key: "owner",
label: _("Owner")
}];
}
componentDidMount(){
document.addEventListener("onProjectListTagClicked", this.addTagAndSearch);
}
componentWillUnmount(){
document.removeEventListener("onProjectListTagClicked", this.addTagAndSearch);
}
closeSearch = () => {
this.searchContainer.classList.remove("open");
}
toggleSearch = e => {
e.stopPropagation();
setTimeout(() => {
this.searchInput.focus();
}, 50);
}
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 => {
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,
search: this.state.searchText.replace(/#/g, ":")
});
}
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;
let paginator = null;
let clearSearch = null;
let toolbar = (<ul className={"pagination pagination-sm toolbar " + (totalItems == 0 && !searchText ? "hidden " : " ") + (totalItems / itemsPerPage <= 1 ? "no-margin" : "")}>
<li className="btn-group" ref={domNode => { this.searchContainer = domNode; }}>
<a href="javascript:void(0);" className="dropdown-toggle"
data-toggle-outside
data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"
onClick={this.toggleSearch}
title={_("Search")}><i className="fa fa-search"></i></a>
<ul className="dropdown-menu dropdown-menu-right search-popup">
<li>
<input type="text"
ref={(domNode) => { this.searchInput = domNode}}
className="form-control search theme-border-secondary-07"
placeholder={_("Search names or #tags")}
spellCheck="false"
autoComplete="false"
value={searchText}
onKeyDown={this.handleSearchKeyDown}
onChange={this.handleSearchChange} />
<button onClick={this.search} className="btn btn-sm btn-default"><i className="fa fa-search"></i></button>
</li>
</ul>
</li>
<li className="btn-group">
<a href="javascript:void(0);" className="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i className="fa fa-sort-alpha-down" title={_("Sort")}></i></a>
<SortPanel selected={this.state.sortKey} items={this.sortItems} onChange={this.sortChanged} />
</li>
</ul>);
if (this.props.currentSearch){
let currentSearch = decodeSearch(this.props.currentSearch);
clearSearch = (<span className="clear-search">{_("Search results for:")} <span className="query">{currentSearch}</span> <a href="javascript:void(0);" onClick={this.clearSearch}>×</a></span>);
}
if (itemsPerPage && itemsPerPage && totalItems > itemsPerPage){
const numPages = Math.ceil(totalItems / itemsPerPage),
pages = [...Array(numPages).keys()]; // [0, 1, 2, ...numPages]
paginator = (
<div className={this.props.className}>
<ul className="pagination pagination-sm">
<li className={currentPage === 1 ? "disabled" : ""}>
<Link to={{search: "?page=1"}}>
<span>&laquo;</span>
</Link>
</li>
{pages.map(page => {
return (<li
key={page + 1}
className={currentPage === (page + 1) ? "active" : ""}
><Link to={{search: "?page=" + (page + 1)}}>{page + 1}</Link></li>);
})}
<li className={currentPage === numPages ? "disabled" : ""}>
<Link to={{search: "?page=" + numPages}}>
<span>&raquo;</span>
</Link>
</li>
</ul>
</div>
);
<ul className="pagination pagination-sm">
<li className={currentPage === 1 ? "disabled" : ""}>
<Link to={{search: this.getQueryForPage(1)}}>
<span>&laquo;</span>
</Link>
</li>
{pages.map(page => {
return (<li
key={page + 1}
className={currentPage === (page + 1) ? "active" : ""}
><Link to={{search: this.getQueryForPage(page + 1)}}>{page + 1}</Link></li>);
})}
<li className={currentPage === numPages ? "disabled" : ""}>
<Link to={{search: this.getQueryForPage(numPages)}}>
<span>&raquo;</span>
</Link>
</li>
</ul>
);
}
return (<div>
{paginator}
{this.props.children}
{paginator}
</div>);
return [
<div key="0" className="text-right paginator">{clearSearch}{toolbar}{paginator}</div>,
this.props.children,
<div key="2" className="text-right paginator">{paginator}</div>,
];
}
}
export default Paginator;
export default withRouter(Paginator);

Wyświetl plik

@ -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();
}
}
@ -101,8 +117,8 @@ class ProjectList extends Paginated {
}else{
return (<div className="project-list">
<ErrorMessage bind={[this, 'error']} />
<Paginator className="text-right" {...this.state.pagination} {...this.props}>
<ul className={"list-group project-list " + (this.state.refreshing ? "refreshing" : "")}>
<Paginator {...this.state.pagination} {...this.props}>
<ul key="1" className={"list-group project-list " + (this.state.refreshing ? "refreshing" : "")}>
{this.state.projects.map(p => (
<ProjectListItem
ref={(domNode) => { this["projectListItem_" + p.id] = domNode }}

Wyświetl plik

@ -7,11 +7,13 @@ import ImportTaskPanel from './ImportTaskPanel';
import UploadProgressBar from './UploadProgressBar';
import ErrorMessage from './ErrorMessage';
import EditProjectDialog from './EditProjectDialog';
import SortPanel from './SortPanel';
import Dropzone from '../vendor/dropzone';
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';
@ -37,9 +39,24 @@ class ProjectListItem extends React.Component {
data: props.data,
refreshing: false,
importing: false,
buttons: []
buttons: [],
sortKey: "-created_at",
filterTags: [],
selectedTags: [],
filterText: ""
};
this.sortItems = [{
key: "created_at",
label: _("Created on")
},{
key: "name",
label: _("Name")
},{
key: "tags",
label: _("Tags")
}];
this.toggleTaskList = this.toggleTaskList.bind(this);
this.closeUploadError = this.closeUploadError.bind(this);
this.cancelUpload = this.cancelUpload.bind(this);
@ -75,6 +92,13 @@ class ProjectListItem extends React.Component {
if (this.refreshRequest) this.refreshRequest.abort();
}
componentDidUpdate(prevProps, prevState){
if (prevState.filterText !== this.state.filterText ||
prevState.selectedTags.length !== this.state.selectedTags.length){
if (this.taskList) this.taskList.applyFilter(this.state.filterText, this.state.selectedTags);
}
}
getDefaultUploadState(){
return {
uploading: false,
@ -383,6 +407,7 @@ class ProjectListItem extends React.Component {
data: JSON.stringify({
name: project.name,
description: project.descr,
tags: project.tags,
permissions: project.permissions
}),
dataType: 'json',
@ -467,10 +492,68 @@ class ProjectListItem extends React.Component {
});
}
sortChanged = key => {
if (this.taskList){
this.setState({sortKey: key});
setTimeout(() => {
this.taskList.refresh();
}, 0);
}
}
handleTagClick = tag => {
return e => {
const evt = new CustomEvent("onProjectListTagClicked", { detail: tag });
document.dispatchEvent(evt);
}
}
tagsChanged = (filterTags) => {
this.setState({filterTags, selectedTags: []});
}
handleFilterTextChange = e => {
this.setState({filterText: e.target.value});
}
toggleTag = t => {
return () => {
if (this.state.selectedTags.indexOf(t) === -1){
this.setState(update(this.state, { selectedTags: {$push: [t]} }));
}else{
this.setState({selectedTags: this.state.selectedTags.filter(tag => tag !== t)});
}
}
}
selectTag = t => {
if (this.state.selectedTags.indexOf(t) === -1){
this.setState(update(this.state, { selectedTags: {$push: [t]} }));
}
}
clearFilter = () => {
this.setState({
filterText: "",
selectedTags: []
});
}
onOpenFilter = () => {
if (this.state.filterTags.length === 0){
setTimeout(() => {
this.filterTextInput.focus();
}, 0);
}
}
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);
let deleteWarning = _("All tasks, images and models associated with this project will be permanently deleted. Are you sure you want to continue?");
if (!data.owned) deleteWarning = _("This project was shared with you. It will not be deleted, but simply hidden from your dashboard. Continue?")
return (
<li className={"project-list-item list-group-item " + (refreshing ? "refreshing" : "")}
@ -490,6 +573,8 @@ class ProjectListItem extends React.Component {
projectName={data.name}
projectDescr={data.description}
projectId={data.id}
projectTags={data.tags}
deleteWarning={deleteWarning}
saveAction={this.updateProject}
showPermissions={this.hasPermission("change")}
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
@ -524,14 +609,13 @@ class ProjectListItem extends React.Component {
<i className="glyphicon glyphicon-remove-circle"></i>
Cancel Upload
</button>
<button type="button" className="btn btn-default btn-sm" onClick={this.viewMap}>
<i className="fa fa-globe"></i> {_("View Map")}
</button>
</div>
<div className="project-name">
{data.name}
{userTags.length > 0 ?
userTags.map((t, i) => <div key={i} className="tag-badge small-badge" onClick={this.handleTagClick(t)}>{t}</div>)
: ""}
</div>
<div className="project-description">
{data.description}
@ -540,17 +624,65 @@ class ProjectListItem extends React.Component {
{numTasks > 0 ?
<span>
<i className='fa fa-tasks'></i>
<a href="javascript:void(0);" onClick={this.toggleTaskList}>
<a href="javascript:void(0);" onClick={this.toggleTaskList}>
{interpolate(_("%(count)s Tasks"), { count: numTasks})} <i className={'fa fa-caret-' + (this.state.showTaskList ? 'down' : 'right')}></i>
</a>
</span>
: ""}
{this.state.showTaskList && numTasks > 1 ?
<div className="task-filters">
<div className="btn-group">
{this.state.selectedTags.length || this.state.filterText !== "" ?
<a className="quick-clear-filter" href="javascript:void(0)" onClick={this.clearFilter}>×</a>
: ""}
<i className='fa fa-filter'></i>
<a href="javascript:void(0);" onClick={this.onOpenFilter} className="dropdown-toggle" data-toggle-outside data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{_("Filter")}
</a>
<ul className="dropdown-menu dropdown-menu-right filter-dropdown">
<li className="filter-text-container">
<input type="text" className="form-control filter-text theme-border-secondary-07"
value={this.state.filterText}
ref={domNode => {this.filterTextInput = domNode}}
placeholder=""
spellCheck="false"
autoComplete="false"
onChange={this.handleFilterTextChange} />
</li>
{filterTags.map(t => <li key={t} className="tag-selection">
<input type="checkbox"
className="filter-checkbox"
id={"filter-tag-" + data.id + "-" + t}
checked={this.state.selectedTags.indexOf(t) !== -1}
onChange={this.toggleTag(t)} /> <label className="filter-checkbox-label" htmlFor={"filter-tag-" + data.id + "-" + t}>{t}</label>
</li>)}
<li className="clear-container"><input type="button" onClick={this.clearFilter} className="btn btn-default btn-xs" value={_("Clear")}/></li>
</ul>
</div>
<div className="btn-group">
<i className='fa fa-sort-alpha-down'></i>
<a href="javascript:void(0);" className="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{_("Sort")}
</a>
<SortPanel selected="-created_at" items={this.sortItems} onChange={this.sortChanged} />
</div>
</div> : ""}
{numTasks > 0 ?
[<i key="edit-icon" className='fa fa-globe'></i>
,<a key="edit-text" href="javascript:void(0);" onClick={this.viewMap}>
{_("View Map")}
</a>]
: ""}
{canEdit ?
[<i key="edit-icon" className='far fa-edit'></i>
,<a key="edit-text" href="javascript:void(0);" onClick={this.handleEditProject}> {_("Edit")}
</a>]
: ""}
</div>
</div>
<i className="drag-drop-icon fa fa-inbox"></i>
@ -586,10 +718,12 @@ class ProjectListItem extends React.Component {
{this.state.showTaskList ?
<TaskList
ref={this.setRef("taskList")}
source={`/api/projects/${data.id}/tasks/?ordering=-created_at`}
source={`/api/projects/${data.id}/tasks/?ordering=${this.state.sortKey}`}
onDelete={this.taskDeleted}
onTaskMoved={this.taskMoved}
hasPermission={this.hasPermission}
onTagsChanged={this.tagsChanged}
onTagClicked={this.selectTag}
history={this.props.history}
/> : ""}

Wyświetl plik

@ -0,0 +1,64 @@
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: () => {},
selected: null
};
static propTypes = {
items: PropTypes.arrayOf(PropTypes.object),
onChange: PropTypes.func,
selected: PropTypes.string
};
constructor(props){
super(props);
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) => {
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 (<ul className="dropdown-menu dropdown-menu-right sort-items">
<li className="sort-order-label">{_("Descending")}</li>
{this.state.items.map(i =>
<li key={i.key}><a onClick={this.handleClick(i.key, "desc")} className="sort-item">
{ i.label } {i.selected === "desc" ? <i className="fa fa-check"></i> : ""}
</a></li>
)}
<li className="sort-order-label">{_("Ascending")}</li>
{this.state.items.map(i =>
<li key={i.key}><a onClick={this.handleClick(i.key, "asc")} className="sort-item">
{ i.label } {i.selected === "asc" ? <i className="fa fa-check"></i> : ""}
</a></li>
)}
</ul>);
}
}
export default SortPanel;

Wyświetl plik

@ -0,0 +1,225 @@
import React from 'react';
import '../css/TagsField.scss';
import PropTypes from 'prop-types';
import update from 'immutability-helper';
import { _ } from '../classes/gettext';
import Tags from '../classes/Tags';
class TagsField extends React.Component {
static defaultProps = {
tags: [],
onUpdate: () => {}
};
static propTypes = {
tags: PropTypes.arrayOf(PropTypes.string),
onUpdate: PropTypes.func
};
constructor(props){
super(props);
this.state = {
userTags: Tags.userTags(props.tags),
systemTags: Tags.systemTags(props.tags)
}
this.dzList = [];
this.domTags = [];
}
componentDidUpdate(){
this.props.onUpdate(Tags.combine(this.state.userTags, this.state.systemTags));
}
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 => {
if (e.key === "Tab" || e.key === "Enter" || e.key === "," || e.key === " "){
e.preventDefault();
e.stopPropagation();
this.addTag();
}else if (e.key === "Backspace" && this.inputText.innerText === ""){
this.removeTag(this.state.userTags.length - 1);
}
}
focus = () => {
this.inputText.focus();
}
stop = e => {
e.stopPropagation();
}
handleRemoveTag = idx => {
return e => {
e.stopPropagation();
this.removeTag(idx);
}
}
removeTag = idx => {
this.setState(update(this.state, { userTags: { $splice: [[idx, 1]] } }));
}
addTag = () => {
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, {
userTags: {$push: [text]}
}));
}
}
this.inputText.innerText = "";
}
}
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 { userTags } = this.state;
if (moveTag){
const dragIdx = userTags.indexOf(dragTag);
const moveIdx = userTags.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;
userTags.splice(insertIdx, 0, dragTag);
for (let i = 0; i < userTags.length; i++){
if (userTags[i] === dragTag && i !== insertIdx){
userTags.splice(i, 1);
break;
}
}
this.setState({userTags});
}
}
}
}
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 { userTags } = this.state;
// 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 = userTags[i];
minDistX = sqDistX;
}
}
});
let side = "right";
if (closestTag){
const b = this.domTags[this.state.userTags.indexOf(closestTag)].getBoundingClientRect();
const centerX = b.x + b.width / 2.0;
if (clientX < centerX) side = "left";
}
return [closestTag, side];
}
render() {
return (<div
ref={domNode => 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.userTags.map((tag, i) =>
<div draggable="true" className="tag-badge" key={i} ref={domNode => this.domTags[i] = domNode}
onClick={this.stop}
onDragStart={this.handleDragStart(tag)}
onDragEnd={this.handleDragEnd}>{tag} <a href="javascript:void(0)" onClick={this.handleRemoveTag(i)}>×</a>&nbsp;&nbsp;</div>
)}
<div className="inputText" contentEditable="true" ref={(domNode) => this.inputText = domNode}
onKeyDown={this.handleKeyDown}
onBlur={this.addTag}></div>
</div>);
}
}
export default TagsField;

Wyświetl plik

@ -11,7 +11,9 @@ class TaskList extends React.Component {
source: PropTypes.string.isRequired, // URL where to load task list
onDelete: PropTypes.func,
onTaskMoved: PropTypes.func,
hasPermission: PropTypes.func.isRequired
hasPermission: PropTypes.func.isRequired,
onTagsChanged: PropTypes.func,
onTagClicked: PropTypes.func
}
constructor(props){
@ -20,7 +22,9 @@ class TaskList extends React.Component {
this.state = {
tasks: [],
error: "",
loading: true
loading: true,
filterText: "",
filterTags: []
};
this.refresh = this.refresh.bind(this);
@ -41,12 +45,19 @@ class TaskList extends React.Component {
this.refresh();
}
applyFilter(text, tags){
this.setState({filterText: text, filterTags: tags});
}
loadTaskList(){
this.setState({loading: true});
this.taskListRequest =
$.getJSON(this.props.source, json => {
this.setState({
tasks: json
});
setTimeout(() => this.notifyTagsChanged(), 0);
})
.fail((jqXHR, textStatus, errorThrown) => {
this.setState({
@ -76,6 +87,49 @@ class TaskList extends React.Component {
if (this.props.onTaskMoved) this.props.onTaskMoved(task);
}
notifyTagsChanged = () => {
const { tasks } = this.state;
const tags = [];
if (tasks){
tasks.forEach(t => {
if (t.tags){
t.tags.forEach(x => {
if (tags.indexOf(x) === -1) tags.push(x);
});
}
});
}
tags.sort();
if (this.props.onTagsChanged) this.props.onTagsChanged(tags);
}
taskEdited = (task) => {
// Update
const { tasks } = this.state;
for (let i = 0; i < tasks.length; i++){
if (tasks[i].id === task.id){
tasks[i] = task;
break;
}
}
this.setState({tasks});
// Tags might have changed
setTimeout(() => this.notifyTagsChanged(), 0);
}
arrayContainsAll = (a, b) => {
let miss = false;
for (let i = 0; i < b.length; i++){
if (a.indexOf(b[i]) === -1){
miss = true;
break;
}
}
return !miss;
}
render() {
let message = "";
if (this.state.loading){
@ -88,9 +142,11 @@ class TaskList extends React.Component {
return (
<div className="task-list">
{message}
{this.state.tasks.map(task => (
{this.state.tasks.filter(t => {
const name = t.name !== null ? t.name : interpolate(_("Task #%(number)s"), { number: t.id });
return name.toLocaleLowerCase().indexOf(this.state.filterText.toLocaleLowerCase()) !== -1 &&
this.arrayContainsAll(t.tags, this.state.filterTags);
}).map(task => (
<TaskListItem
data={task}
key={task.id}
@ -98,9 +154,13 @@ class TaskList extends React.Component {
onDelete={this.deleteTask}
onMove={this.moveTask}
onDuplicate={this.refresh}
onEdited={this.taskEdited}
onTagClicked={this.props.onTagClicked}
hasPermission={this.props.hasPermission}
history={this.props.history} />
))}
{message}
</div>
);
}

Wyświetl plik

@ -12,6 +12,7 @@ import TaskPluginActionButtons from './TaskPluginActionButtons';
import MoveTaskDialog from './MoveTaskDialog';
import PipelineSteps from '../classes/PipelineSteps';
import Css from '../classes/Css';
import Tags from '../classes/Tags';
import Trans from './Trans';
import { _, interpolate } from '../classes/gettext';
@ -23,7 +24,9 @@ class TaskListItem extends React.Component {
onDelete: PropTypes.func,
onMove: PropTypes.func,
onDuplicate: PropTypes.func,
hasPermission: PropTypes.func
hasPermission: PropTypes.func,
onEdited: PropTypes.func,
onTagClicked: PropTypes.func
}
constructor(props){
@ -278,6 +281,7 @@ class TaskListItem extends React.Component {
handleEditTaskSave(task){
this.setState({task, editing: false});
if (this.props.onEdited) this.props.onEdited(task);
this.setAutoRefresh();
}
@ -401,6 +405,12 @@ class TaskListItem extends React.Component {
}else return false;
}
handleTagClick = t => {
return () => {
if (this.props.onTagClicked) this.props.onTagClicked(t);
}
}
render() {
const task = this.state.task;
const name = task.name !== null ? task.name : interpolate(_("Task #%(number)s"), { number: task.id });
@ -596,7 +606,7 @@ class TaskListItem extends React.Component {
{showExitedWithCodeOneHints ?
<div className="task-warning"><i className="fa fa-info-circle"></i> <div className="inline">
<Trans params={{link1: `<a href="https://www.dronedb.app/" target="_blank">DroneDB</a>`, link2: `<a href="https://drive.google.com/drive/u/0/" target="_blank">Google Drive</a>`, open_a_topic: `<a href="http://community.opendronemap.org/c/webodm" target="_blank">${_("open a topic")}</a>`, }}>{_("\"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!")}</Trans> <i className="far fa-smile"></i>
<Trans params={{link: `<a href="https://docs.opendronemap.org" target="_blank">docs.opendronemap.org</a>` }}>{_("\"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)")}</Trans>
</div>
</div>
: ""}
@ -706,6 +716,7 @@ class TaskListItem extends React.Component {
let taskActionsIcon = "fa-ellipsis-h";
if (actionLoading) taskActionsIcon = "fa-circle-notch fa-spin fa-fw";
const userTags = Tags.userTags(task.tags);
return (
<div className="task-list-item">
@ -719,7 +730,10 @@ class TaskListItem extends React.Component {
: ""}
<div className="row">
<div className="col-sm-5 col-xs-12 name">
<i onClick={this.toggleExpanded} className={"clickable far " + (this.state.expanded ? "fa-minus-square" : " fa-plus-square")}></i> <a href="javascript:void(0);" onClick={this.toggleExpanded}>{name}</a>
<i onClick={this.toggleExpanded} className={"clickable far " + (this.state.expanded ? "fa-minus-square" : " fa-plus-square")}></i> <a href="javascript:void(0);" onClick={this.toggleExpanded} className="name-link">{name}</a>
{userTags.length > 0 ?
userTags.map((t, i) => <div key={i} className="tag-badge small-badge" onClick={this.handleTagClick(t)}>{t}</div>)
: ""}
</div>
<div className="col-sm-1 col-xs-5 details">
<i className="far fa-image"></i> {task.images_count}

Wyświetl plik

@ -0,0 +1,15 @@
import React from 'react';
import { shallow } from 'enzyme';
import SortPanel from '../SortPanel';
var sortItems = [{
key: "created_at",
label: "Created on"
}];
describe('<SortPanel />', () => {
it('renders without exploding', () => {
const wrapper = shallow(<SortPanel items={sortItems} selected="created_at" />);
expect(wrapper.exists()).toBe(true);
})
});

Wyświetl plik

@ -0,0 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import TagsField from '../TagsField';
describe('<TagsField />', () => {
it('renders without exploding', () => {
const wrapper = shallow(<TagsField tags={["abc"]} />);
expect(wrapper.exists()).toBe(true);
})
});

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -28,8 +28,22 @@
.name-loading{
position: absolute;
right: 30px;
right: 60px;
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;
}
}
}

Wyświetl plik

@ -0,0 +1,59 @@
.paginator{
display: flex;
justify-content: flex-end;
margin-bottom: 8px;
.toolbar{
i{
opacity: 0.8;
}
margin-right: 8px;
&.no-margin{
margin-right: 0;
}
}
.btn-group.open > .dropdown-menu{
top: 22px;
a{
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;
}
}
}

Wyświetl plik

@ -12,6 +12,10 @@
}
}
.project-description{
min-height: 12px;
}
.drag-drop-icon{
display: none;
position: absolute;
@ -97,4 +101,71 @@
}
}
}
.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;
&:hover{
cursor: pointer;
}
}
.filter-dropdown{
max-width: 320px;
padding-bottom: 6px;
}
.filter-text{
height: 25px;
margin-left: 7px;
margin-right: 6px;
margin-bottom: 4px;
padding-left: 4px;
padding-right: 4px;
border-width: 1px;
border-radius: 3px;
display: block;
width: 100%;
}
.filter-text-container,.tag-selection{
display: flex;
}
.filter-checkbox{
margin-left: 8px;
}
.filter-checkbox-label{
font-weight: normal;
position: relative;
top: 4px;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 4px;
width: 100%;
}
.clear-container{
text-align: right;
margin-top: 2px;
margin-right: 6px;
}
.quick-clear-filter{
margin-right: 6px !important;
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -0,0 +1,40 @@
.tags-field{
height: auto;
padding-bottom: 2px;
&:hover{
cursor: text;
}
.tag-badge{
&:hover{
cursor: grab;
}
display: inline-block;
width: auto;
padding-left: 6px;
padding-top: 2px;
padding-bottom: 2px;
margin-top: -2px;
margin-right: 4px;
margin-bottom: 8px;
border-radius: 6px;
a{
margin-top: 2px;
font-weight: bold;
padding-bottom: 5px;
}
a:hover, a:focus, a:active{
cursor: pointer;
text-decoration: none !important;
}
}
.inputText{
display: inline-block;
outline: none;
border: none;
margin-bottom: 10px;
min-width: 1px;
}
}

Wyświetl plik

@ -1,2 +1,5 @@
.task-list{
.task-bar{
text-align: right;
}
}

Wyświetl plik

@ -119,4 +119,23 @@
.mb{
margin-bottom: 12px;
}
.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;
}
.name-link{
margin-right: 4px;
}
}

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 5.0 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 6.0 KiB

Wyświetl plik

@ -1,93 +1,93 @@
// Auto-generated with extract_odm_strings.py, do not edit!
_("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");
_("Use this tag if you have a GCP File but want to use the EXIF information for georeferencing instead. Default: %(default)s");
_("Generate static tiles for orthophotos and DEMs that are suitable for viewers like Leaflet or OpenLayers. Default: %(default)s");
_("Simple Morphological Filter window radius parameter (meters). Default: %(default)s");
_("The maximum vertex count of the output mesh. 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");
_("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");
_("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");
_("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");
_("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");
_("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");
_("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");
_("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");
_("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:<code> or <+proj definition>geo_x geo_y geo_z im_x im_y image_name [gcp_name] [extra1] [extra2]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");
_("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.");
_("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");
_("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");
_("Maximum number of frames to extract from video files for processing. Set to 0 for no limit. 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");
_("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 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");
_("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");
_("Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s");
_("Octree depth used in the mesh reconstruction, increase to get more vertices, recommended values are 8-12. Default: %(default)s");
_("Skip normalization of colors across all images. Useful when processing radiometric data. 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");
_("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");
_("Create Cloud-Optimized GeoTIFFs instead of normal GeoTIFFs. 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 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 dont have values for omega/phi/kappa you can set them to 0. The file needs to use the following format: EPSG:<code> or <+proj definition>image_name geo_x geo_y geo_z [omega (degrees)] [phi (degrees)] [kappa (degrees)] [horz accuracy (meters)] [vert accuracy (meters)]Default: %(default)s");
_("Set this parameter if you want to generate a PNG rendering of the orthophoto. Default: %(default)s");
_("Choose the algorithm for extracting keypoints and computing descriptors. Can be one of: %(choices)s. Default: %(default)s");
_("Generate OGC 3D Tiles outputs. 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");
_("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");
_("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");
_("Simple Morphological Filter elevation scalar parameter. 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 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");
_("Rerun this stage only and stop. 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.");
_("Generate single file Binary glTF (GLB) textured models. Default: %(default)s");
_("URL to a ClusterODM instance for distributing a split-merge workflow on multiple nodes in parallel. 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");
_("Simple Morphological Filter elevation threshold parameter (meters). 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");
_("Export the georeferenced point cloud in CSV format. 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");
_("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");
_("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");
_("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");
_("Skip the blending of colors near seams. 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");
_("The maximum output resolution of extracted video frames in pixels. 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");
_("Path to a GeoTIFF DEM or a LAS/LAZ point cloud that the reconstruction outputs should be automatically aligned to. Experimental. Default: %(default)s");
_("Rerun processing from this stage. Can be one of: %(choices)s. 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");
_("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");
_("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");
_("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");
_("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.");
_("Build orthophoto overviews for faster display in programs such as QGIS. 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");
_("show this help message and exit");
_("Do not use GPU acceleration, even if it's available. Default: %(default)s");
_("Skip generation of the orthophoto. This can save time if you only need 3D results or DEMs. Default: %(default)s");
_("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");
_("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");
_("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");
_("Automatically compute image masks using AI to remove the sky. Experimental. 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");
_("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");
_("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");
_("End processing at this stage. 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");
_("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");
_("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");
_("Set this parameter if you want to generate a Google Earth (KMZ) rendering of the orthophoto. Default: %(default)s");
_("Export the georeferenced point cloud in LAS format. Default: %(default)s");
_("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");
_("Set the compression to use for orthophotos. Can be one of: %(choices)s. Default: %(default)s");
_("Generate OBJs that have a single material and a single texture file instead of multiple ones. Default: %(default)s");
_("Keep faces in the mesh that are not seen in any camera. 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");
_("Path to the file containing the ground control points used for georeferencing. The file needs to use the following format: EPSG:<code> or <+proj definition>geo_x geo_y geo_z im_x im_y image_name [gcp_name] [extra1] [extra2]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 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");
_("Displays version number and exits. ");
_("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:<code> 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");

Wyświetl plik

@ -1,5 +1,5 @@
/*!
* Bootstrap v3.3.1 (http://getbootstrap.com)
* Bootstrap v3.3.1 (http://getbootstrap.com) modified to allow "data-toggle-outside"
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
@ -838,6 +838,15 @@ if (typeof jQuery === 'undefined') {
if (!$parent.hasClass('open')) return
// Modification to allow toggling only with click outside
if ($this.attr('data-toggle-outside')){
if (e && e.target){
var sibiling = $this.get(0).nextSibling;
if (sibiling === e.target || sibiling.contains(e.target)) return
}
}
// End modification
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
if (e.isDefaultPrevented()) return

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -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() {

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 14 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 30 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 7.8 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 535 B

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.4 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 40 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 65 KiB

Wyświetl plik

@ -1,127 +0,0 @@
/*
Leaflet.AwesomeMarkers, a plugin that adds colorful iconic markers for Leaflet, based on the Font Awesome icons
(c) 2012-2013, Lennard Voogdt
http://leafletjs.com
https://github.com/lvoogdt
*/
/*global L*/
import "./leaflet.awesome-markers.css";
(function (window, document, undefined) {
"use strict";
/*
* Leaflet.AwesomeMarkers assumes that you have already included the Leaflet library.
*/
L.AwesomeMarkers = {};
L.AwesomeMarkers.version = '2.0.1';
L.AwesomeMarkers.Icon = L.Icon.extend({
options: {
iconSize: [35, 45],
iconAnchor: [17, 42],
popupAnchor: [1, -32],
shadowAnchor: [10, 12],
shadowSize: [36, 16],
className: 'awesome-marker',
prefix: 'glyphicon',
spinClass: 'fa-spin',
extraClasses: '',
icon: 'home',
markerColor: 'blue',
iconColor: 'white'
},
initialize: function (options) {
options = L.Util.setOptions(this, options);
},
createIcon: function () {
var div = document.createElement('div'),
options = this.options;
if (options.icon) {
div.innerHTML = this._createInner();
}
if (options.bgPos) {
div.style.backgroundPosition =
(-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px';
}
this._setIconStyles(div, 'icon-' + options.markerColor);
return div;
},
_createInner: function() {
var iconClass, iconSpinClass = "", iconColorClass = "", iconColorStyle = "", options = this.options;
if(options.icon.slice(0,options.prefix.length+1) === options.prefix + "-") {
iconClass = options.icon;
} else {
iconClass = options.prefix + "-" + options.icon;
}
if(options.spin && typeof options.spinClass === "string") {
iconSpinClass = options.spinClass;
}
if(options.iconColor) {
if(options.iconColor === 'white' || options.iconColor === 'black') {
iconColorClass = "icon-" + options.iconColor;
} else {
iconColorStyle = "style='color: " + options.iconColor + "' ";
}
}
return "<i " + iconColorStyle + "class='" + options.extraClasses + " " + options.prefix + " " + iconClass + " " + iconSpinClass + " " + iconColorClass + "'></i>";
},
_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));

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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);
})));

Wyświetl plik

@ -33,7 +33,7 @@
<ul>
<li>{% trans 'You need at least 5 images, but 16-32 is typically the minimum.' %}</li>
<li>{% trans 'Images must overlap by 65% or more. Aim for 70-72%' %}</li>
<li>{% trans 'For great 3D, images must overlap by 83%' %}</li>
<li>{% trans 'For great 3D, images must overlap by 83%' %}</li>
<li>{% blocktrans with link_start='<a href="https://github.com/OpenDroneMap/OpenDroneMap/wiki/Running-OpenDroneMap#running-odm-with-ground-control" target="_blank">' link_end='</a>' %}A {{link_start}}GCP File{{link_end}} is optional, but can increase georeferencing accuracy{% endblocktrans %}</li>
</ul>
</p>

Wyświetl plik

@ -68,6 +68,9 @@ class TestApiProjects(BootTestCase):
# Other user can see project
res = other_client.get("/api/projects/{}/".format(project.id))
self.assertEqual(res.status_code, status.HTTP_200_OK)
# Other user does not own the project
self.assertFalse(res.data['owned'])
# Other user still cannot edit project
res = other_client.post("/api/projects/{}/edit/".format(project.id), {
@ -103,6 +106,9 @@ class TestApiProjects(BootTestCase):
res = client.get("/api/projects/{}/".format(project.id))
self.assertEqual(res.status_code, status.HTTP_200_OK)
# Current user owns the project
self.assertTrue(res.data['owned'])
perms = get_perms(user, project)
self.assertEqual(len(perms), 4)

Wyświetl plik

@ -0,0 +1,93 @@
import logging
import json
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APIClient
from app.models import Project, Task
from app.tests.classes import BootTestCase
logger = logging.getLogger('app.logger')
class TestApiPreset(BootTestCase):
def setUp(self):
super().setUp()
def test_tags(self):
client = APIClient()
client.login(username="testuser", password="test1234")
user = User.objects.get(username="testuser")
project = Project.objects.create(
owner=user,
name="test project",
tags="a b c .hidden"
)
# Can retrieve tags
res = client.get("/api/projects/{}/".format(project.id))
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(4, len(res.data['tags']))
# Can update tags
res = client.post("/api/projects/{}/edit/".format(project.id), {
'tags': ["b", "c", ".hidden"]
}, format="json")
self.assertEqual(res.status_code, status.HTTP_200_OK)
project.refresh_from_db()
self.assertEqual(project.tags, "b c .hidden")
# Can search projects by tag
project2 = Project.objects.create(
owner=user,
name="test project2",
tags="c d"
)
res = client.get("/api/projects/?search=:c")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(2, len(res.data))
res = client.get("/api/projects/?search=:d")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(1, len(res.data))
# Can search projects by name
res = client.get("/api/projects/?search=project2")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(1, len(res.data))
Task.objects.create(project=project, name="TestTask0")
task = Task.objects.create(project=project, name="TestTask1", tags="d .hidden")
task2 = Task.objects.create(project=project2, name="TestTask2", tags="ee .hidden")
# Can retrieve task tags
res = client.get("/api/projects/{}/tasks/{}/".format(project.id, task.id))
self.assertEqual(2, len(res.data['tags']))
# Can update task tags
res = client.patch("/api/projects/{}/tasks/{}/".format(project.id, task.id), {
'tags': ["d", "e", ".hidden"]
}, format="json")
self.assertTrue(res.status_code == status.HTTP_200_OK)
task.refresh_from_db()
self.assertEqual(task.tags, "d e .hidden")
# Can search task tags
res = client.get("/api/projects/?search=::e")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(1, len(res.data))
self.assertEqual(res.data[0]['tasks'][0], task.id)
res = client.get("/api/projects/?search=::hidden")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(2, len(res.data))
# Can search task names
res = client.get("/api/projects/?search=TestTask2")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(1, len(res.data))
self.assertEqual(res.data[0]['tasks'][0], task2.id)

Wyświetl plik

@ -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()))

Wyświetl plik

@ -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

Wyświetl plik

@ -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()

Wyświetl plik

@ -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
@ -208,17 +208,17 @@ 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:
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()

Wyświetl plik

@ -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

Wyświetl plik

@ -1,6 +1,7 @@
from app.plugins import PluginBase, Menu, MountPoint
from django.shortcuts import render
from django.utils.translation import gettext as _
from django.contrib.auth.decorators import login_required
class Plugin(PluginBase):
@ -8,8 +9,12 @@ class Plugin(PluginBase):
return [Menu(_("GCP Interface"), self.public_url(""), "fa fa-map-marker-alt fa-fw")]
def app_mount_points(self):
@login_required
def gcpi(request):
return render(request, self.template_path("app.html"), {'title': 'GCP Editor'})
return [
MountPoint('$', lambda request: render(request, self.template_path("app.html"), {'title': 'GCP Editor'}))
MountPoint('$', gcpi)
]

2
locale

@ -1 +1 @@
Subproject commit f5660651b87d1a2248aea9dd139e33bc6a2fd0bc
Subproject commit 12f8546a1779a1e86254a806a2c88661cee07d84

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "WebODM",
"version": "1.9.19",
"version": "2.0.1",
"description": "User-friendly, extendable application and API for processing aerial imagery.",
"main": "index.js",
"scripts": {
@ -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",

Wyświetl plik

@ -113,8 +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[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") &
echo -e "\033[39m") &
}
if [ "$1" = "--setup-devenv" ] || [ "$2" = "--setup-devenv" ] || [ "$1" = "--no-gunicorn" ]; then

Wyświetl plik

@ -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"
@ -460,6 +501,7 @@ elif [[ $1 = "rebuild" ]]; then
echo "Rebuilding WebODM..."
rebuild
elif [[ $1 = "update" ]]; then
environment_check
down
echo "Updating WebODM..."
@ -474,7 +516,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