|
@ -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
|
||||
|
|
|
@ -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 && \
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 []
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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),
|
||||
]
|
|
@ -9,7 +9,7 @@ from webodm import settings
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0015_public_task_uuids'),
|
||||
('app', '0014_public_task_uuids'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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")
|
|
@ -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")
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
/>;
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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>«</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>»</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
<ul className="pagination pagination-sm">
|
||||
<li className={currentPage === 1 ? "disabled" : ""}>
|
||||
<Link to={{search: this.getQueryForPage(1)}}>
|
||||
<span>«</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>»</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);
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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}
|
||||
/> : ""}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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> </div>
|
||||
)}
|
||||
<div className="inputText" contentEditable="true" ref={(domNode) => this.inputText = domNode}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onBlur={this.addTag}></div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
export default TagsField;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
});
|
|
@ -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);
|
||||
})
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,2 +1,5 @@
|
|||
.task-list{
|
||||
.task-bar{
|
||||
text-align: right;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Po Szerokość: | Wysokość: | Rozmiar: 5.0 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 6.0 KiB |
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 14 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 30 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 7.8 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 535 B |
Przed Szerokość: | Wysokość: | Rozmiar: 1.4 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 40 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 65 KiB |
|
@ -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));
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
||||
})));
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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()))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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",
|
||||
|
|
3
start.sh
|
@ -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
|
||||
|
|
64
webodm.sh
|
@ -234,11 +234,49 @@ if [[ $gpu = true ]]; then
|
|||
prepare_intel_render_group
|
||||
fi
|
||||
|
||||
# $1 = command | $2 = help_text | $3 = install_command (optional)
|
||||
docker_compose="docker-compose"
|
||||
check_docker_compose(){
|
||||
dc_msg_ok="\033[92m\033[1m OK\033[0m\033[39m"
|
||||
|
||||
# Check if docker-compose exists
|
||||
hash "docker-compose" 2>/dev/null || not_found=true
|
||||
if [[ $not_found ]]; then
|
||||
# Check if compose plugin is installed
|
||||
if ! docker compose > /dev/null 2>&1; then
|
||||
|
||||
if [ "${platform}" = "Linux" ] && [ -z "$1" ] && [ ! -z "$HOME" ]; then
|
||||
echo -e "Checking for docker compose... \033[93mnot found, we'll attempt to install it\033[39m"
|
||||
check_command "curl" "Cannot automatically install docker compose. Please visit https://docs.docker.com/compose/install/" "" "silent"
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins
|
||||
curl -SL# https://github.com/docker/compose/releases/download/v2.17.2/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
|
||||
check_docker_compose "y"
|
||||
else
|
||||
if [ -z "$1" ]; then
|
||||
echo -e "Checking for docker compose... \033[93mnot found, please visit https://docs.docker.com/compose/install/ to install docker compose\033[39m"
|
||||
else
|
||||
echo -e "\033[93mCannot automatically install docker compose. Please visit https://docs.docker.com/compose/install/\033[39m"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
docker_compose="docker compose"
|
||||
fi
|
||||
else
|
||||
docker_compose="docker-compose"
|
||||
fi
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo -e "Checking for $docker_compose... $dc_msg_ok"
|
||||
fi
|
||||
}
|
||||
|
||||
# $1 = command | $2 = help_text | $3 = install_command (optional) | $4 = silent
|
||||
check_command(){
|
||||
check_msg_prefix="Checking for $1... "
|
||||
check_msg_result="\033[92m\033[1m OK\033[0m\033[39m"
|
||||
|
||||
unset not_found
|
||||
hash "$1" 2>/dev/null || not_found=true
|
||||
if [[ $not_found ]]; then
|
||||
|
||||
|
@ -254,7 +292,10 @@ check_command(){
|
|||
fi
|
||||
fi
|
||||
|
||||
echo -e "$check_msg_prefix $check_msg_result"
|
||||
if [ -z "$4" ]; then
|
||||
echo -e "$check_msg_prefix $check_msg_result"
|
||||
fi
|
||||
|
||||
if [[ $not_found ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
@ -262,7 +303,7 @@ check_command(){
|
|||
|
||||
environment_check(){
|
||||
check_command "docker" "https://www.docker.com/"
|
||||
check_command "docker-compose" "Run \033[1mpip install docker-compose\033[0m" "pip install docker-compose"
|
||||
check_docker_compose
|
||||
}
|
||||
|
||||
run(){
|
||||
|
@ -293,7 +334,7 @@ start(){
|
|||
echo "Make sure to issue a $0 down if you decide to change the environment."
|
||||
echo ""
|
||||
|
||||
command="docker-compose -f docker-compose.yml"
|
||||
command="$docker_compose -f docker-compose.yml"
|
||||
|
||||
if [[ $WO_DEFAULT_NODES -gt 0 ]]; then
|
||||
if [ "${GPU_NVIDIA}" = true ]; then
|
||||
|
@ -365,7 +406,7 @@ start(){
|
|||
}
|
||||
|
||||
down(){
|
||||
command="docker-compose -f docker-compose.yml"
|
||||
command="$docker_compose -f docker-compose.yml"
|
||||
|
||||
if [ "${GPU_NVIDIA}" = true ]; then
|
||||
command+=" -f docker-compose.nodeodm.gpu.nvidia.yml"
|
||||
|
@ -381,10 +422,10 @@ down(){
|
|||
}
|
||||
|
||||
rebuild(){
|
||||
run "docker-compose down --remove-orphans"
|
||||
run "$docker_compose down --remove-orphans"
|
||||
run "rm -fr node_modules/ || sudo rm -fr node_modules/"
|
||||
run "rm -fr nodeodm/external/NodeODM || sudo rm -fr nodeodm/external/NodeODM"
|
||||
run "docker-compose -f docker-compose.yml -f docker-compose.build.yml build --no-cache"
|
||||
run "$docker_compose -f docker-compose.yml -f docker-compose.build.yml build --no-cache"
|
||||
#run "docker images --no-trunc -aqf \"dangling=true\" | xargs docker rmi"
|
||||
echo -e "\033[1mDone!\033[0m You can now start WebODM by running $0 start"
|
||||
}
|
||||
|
@ -403,7 +444,7 @@ run_tests(){
|
|||
echo -e "\033[1mDone!\033[0m Everything looks in order."
|
||||
else
|
||||
echo "Running tests in webapp container"
|
||||
run "docker-compose exec webapp /bin/bash -c \"/webodm/webodm.sh test\""
|
||||
run "$docker_compose exec webapp /bin/bash -c \"/webodm/webodm.sh test\""
|
||||
fi
|
||||
}
|
||||
|
||||
|
@ -434,7 +475,7 @@ elif [[ $1 = "stop" ]]; then
|
|||
environment_check
|
||||
echo "Stopping WebODM..."
|
||||
|
||||
command="docker-compose -f docker-compose.yml"
|
||||
command="$docker_compose -f docker-compose.yml"
|
||||
|
||||
if [ "${GPU_NVIDIA}" = true ]; then
|
||||
command+=" -f docker-compose.nodeodm.gpu.nvidia.yml"
|
||||
|
@ -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
|
||||
|
|