kopia lustrzana https://github.com/OpenDroneMap/WebODM
Merge pull request #1536 from Aero-Ai/usewebodmcomponents
Restore the Cesium Ion Pluginpull/1537/head
commit
58f4d3830f
|
@ -62,7 +62,7 @@ class MapView extends React.Component {
|
|||
let thermalCount = 0;
|
||||
for (let item of this.props.mapItems){
|
||||
if (item.meta && item.meta.task && item.meta.task.orthophoto_bands){
|
||||
if (item.meta.task.orthophoto_bands.length === 2 && item.meta.task.orthophoto_bands &&
|
||||
if (item.meta.task.orthophoto_bands.length === 2 && item.meta.task.orthophoto_bands &&
|
||||
item.meta.task.orthophoto_bands[0] && typeof(item.meta.task.orthophoto_bands[0].description) === "string" &&
|
||||
item.meta.task.orthophoto_bands[0].description.toLowerCase() === "lwir"){
|
||||
thermalCount++;
|
||||
|
@ -102,7 +102,7 @@ class MapView extends React.Component {
|
|||
|
||||
render(){
|
||||
const isThermal = this.isThermalMap();
|
||||
|
||||
|
||||
let mapTypeButtons = [
|
||||
{
|
||||
label: _("Orthophoto"),
|
||||
|
@ -131,13 +131,13 @@ class MapView extends React.Component {
|
|||
|
||||
return (<div className="map-view">
|
||||
<div className="map-view-header">
|
||||
{this.props.title ?
|
||||
{this.props.title ?
|
||||
<h3 className="map-title" title={this.props.title}><i className="fa fa-globe"></i> {this.props.title}</h3>
|
||||
: ""}
|
||||
|
||||
<div className="map-type-selector btn-group" role="group">
|
||||
{mapTypeButtons.map(mapType =>
|
||||
<button
|
||||
<button
|
||||
key={mapType.type}
|
||||
onClick={this.handleMapTypeButton(mapType.type)}
|
||||
title={mapType.label}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<p align="center">
|
||||
<img src="https://github.com/AnalyticalGraphicsInc/Cesium/wiki/logos/Cesium_Logo_Color.jpg" width="50%" />
|
||||
</p>
|
||||
|
||||
# Cesium Ion WebODM Plugin
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
### Overview
|
||||
The Cesium Ion WebODM plugin enables seamless integratIon to upload processed WebODM tasks to your Cesium Ion account.
|
||||
Using the Cesium Ion ecosystem, multi-gigabit models can be streamed to any device using Cesium clients to load 3D tiles.
|
||||
|
||||
Learn more at https://Cesium.com
|
||||
|
||||
### Prerequisites
|
||||
> - WebODM versIon 2.5.0 or later
|
||||
> - [Cesium Ion](https://Cesium.com/ion/tokens) token with `assets:list, assets:read, assets:write` permissions
|
||||
> - Internet connection
|
||||
|
||||
## 2. Initial Setup
|
||||
|
||||
### Enabling Plugin
|
||||
1. Go to "AdministratIon -> Plugins" and enable Cesium ion.
|
||||
2. Select the left Cesium Ion tab
|
||||
3. Copy and paste your Cesium Ion token then `Set Token`.
|
||||
|
||||
## 3. Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Example:
|
||||
1. Create a new project in the WebODM dashboard.
|
||||
2. Upload your images.
|
||||
3. Edit the WebODM task options and make sure to enable `texturing-single-material`.
|
||||
4. Start the WebODM processing (this will take a while to complete).
|
||||
3. Once finished, select the `Tile in CesiumIon` dropdown button for a list of available asset uploads.
|
||||
4. Click on a dropdown item to show the popup dialogue where you can rename the asset, add a description/attribute, or enable an Cesium Ion option before uploading.
|
||||
5. Submit to start the upload to your Cesium Ion assets account.
|
||||
6. You can view the progress of the upload by clicking the `View Ion Tasks` button.
|
||||
7. Once complete you can then click on the `View in Cesium` dropdown button to open a new browser tab to view your Cesium Ion assets
|
||||
|
||||
> **NOTE:** There are 2 phases to a Cesium task: **uploading** and **processing**. Uploading is the transfer of processed WebODM data to Cesium Ion. Processing is the tiling/rendering Cesium Ion does to generate streamable models.
|
||||
|
||||
## 4. New Feature: CesiumIon Plugin v1.3.0
|
||||
|
||||
### KVX 2.0
|
||||
Cesium Ion upgraded their streaming pipeline to automatically use their `1.1` tileset version. The new standardize tileset version comes with [`KTX2`](https://www.khronos.org/ktx/), a texture format compression option to create a smaller tilset for better streaming performance.
|
||||
|
||||
## 5. Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
> - **Issue:** texture model uploads to cesium ion but fails to process/render it.
|
||||
> - **Solution:** Ensure that you have enabled `texturing-single-material` before WebODM processing on a *new* project task as WebODM stores previously processed textured models in the same odm_textured data folder. (Cesium Ion only accepts single textured materials for a 3D_CAPTURE)
|
||||
|
||||
## 6. FAQ
|
||||
|
||||
### Frequently Asked Questions
|
||||
|
||||
> - **Q:** Can I use the plugin with older versions of WebODM?
|
||||
> - **A:** No, the updated plugin is compatible only with WebODM versIon 2.5.0 or later.
|
|
@ -0,0 +1,17 @@
|
|||
[
|
||||
{
|
||||
"name": "formik",
|
||||
"url": "https://github.com/jaredpalmer/formik",
|
||||
"license": "MIT"
|
||||
},
|
||||
{
|
||||
"name": "yup",
|
||||
"url": "https://github.com/jquense/yup",
|
||||
"license": "MIT"
|
||||
},
|
||||
{
|
||||
"name": "boto3",
|
||||
"url": "https://github.com/boto/boto3",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
]
|
|
@ -0,0 +1 @@
|
|||
from .plugin import *
|
|
@ -0,0 +1,322 @@
|
|||
import sys
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
from os import path
|
||||
from enum import Enum
|
||||
from itertools import chain as iter_chain
|
||||
|
||||
from app.plugins.views import TaskView
|
||||
from app.plugins.worker import run_function_async
|
||||
from app.plugins.data_store import GlobalDataStore
|
||||
from app.plugins import signals as plugin_signals
|
||||
|
||||
from worker.celery import app
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.fields import ChoiceField, CharField, JSONField
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework import serializers
|
||||
|
||||
from .globals import PROJECT_NAME, ION_API_URL
|
||||
from .uploader import upload_to_ion
|
||||
|
||||
|
||||
pluck = lambda dic, *keys: [dic[k] if k in dic else None for k in keys]
|
||||
|
||||
|
||||
### ###
|
||||
# API UTILS #
|
||||
### ###
|
||||
def get_key_for(task_id, key):
|
||||
return "task_{}_{}".format(str(task_id), key)
|
||||
|
||||
|
||||
def del_asset_info(task_id, asset_type, ds=None):
|
||||
if ds is None:
|
||||
ds = GlobalDataStore(PROJECT_NAME)
|
||||
ds.del_key(get_key_for(task_id, asset_type.value))
|
||||
|
||||
|
||||
def set_asset_info(task_id, asset_type, json, ds=None):
|
||||
if ds is None:
|
||||
ds = GlobalDataStore(PROJECT_NAME)
|
||||
return ds.set_json(get_key_for(task_id, asset_type.value), json)
|
||||
|
||||
|
||||
def get_asset_info(task_id, asset_type, default=None, ds=None):
|
||||
if default is None:
|
||||
default = {
|
||||
"id": None,
|
||||
"upload": {"progress": 0, "active": False},
|
||||
"process": {"progress": 0, "active": False},
|
||||
"error": "",
|
||||
}
|
||||
if ds is None:
|
||||
ds = GlobalDataStore(PROJECT_NAME)
|
||||
return ds.get_json(get_key_for(task_id, asset_type.value), default)
|
||||
|
||||
|
||||
def is_asset_task(asset_meta):
|
||||
is_error = len(asset_meta["error"]) > 0
|
||||
return asset_meta["upload"]["active"] or asset_meta["process"]["active"] or is_error
|
||||
|
||||
|
||||
def get_processing_assets(task_id):
|
||||
ispc = app.control.inspect()
|
||||
ion_tasks = set()
|
||||
active = set()
|
||||
from uuid import UUID
|
||||
|
||||
for wtask in iter_chain(*ispc.active().values(), *ispc.reserved().values()):
|
||||
args = eval(wtask["args"])
|
||||
if len(args) < 2:
|
||||
continue
|
||||
ion_tasks.add((str(args[0]), AssetType[args[1]]))
|
||||
|
||||
for asset_type in AssetType:
|
||||
asset_info = get_asset_info(task_id, asset_type)
|
||||
ion_task_id = (task_id, asset_type)
|
||||
if not is_asset_task(asset_info) or ion_task_id in ion_tasks:
|
||||
continue
|
||||
active.add(asset_type)
|
||||
|
||||
return active
|
||||
|
||||
|
||||
### ###
|
||||
# MODEL CONFIG #
|
||||
### ###
|
||||
class AssetType(str, Enum):
|
||||
ORTHOPHOTO = "ORTHOPHOTO"
|
||||
TERRAIN_MODEL = "TERRAIN_MODEL"
|
||||
SURFACE_MODEL = "SURFACE_MODEL"
|
||||
POINTCLOUD = "POINTCLOUD"
|
||||
TEXTURED_MODEL = "TEXTURED_MODEL"
|
||||
|
||||
|
||||
class SourceType(str, Enum):
|
||||
RASTER_IMAGERY = "RASTER_IMAGERY"
|
||||
RASTER_TERRAIN = "RASTER_TERRAIN"
|
||||
TERRAIN_DATABASE = "TERRAIN_DATABASE"
|
||||
CITYGML = "CITYGML"
|
||||
KML = "KML"
|
||||
CAPTURE = "3D_CAPTURE"
|
||||
MODEL = "3D_MODEL"
|
||||
POINTCLOUD = "POINT_CLOUD"
|
||||
|
||||
|
||||
class OutputType(str, Enum):
|
||||
IMAGERY = "IMAGERY"
|
||||
TILES = "3DTILES"
|
||||
TERRAIN = "TERRAIN"
|
||||
|
||||
|
||||
ASSET_TO_FILE = {
|
||||
AssetType.ORTHOPHOTO: "orthophoto.tif",
|
||||
AssetType.TERRAIN_MODEL: "dtm.tif",
|
||||
AssetType.SURFACE_MODEL: "dsm.tif",
|
||||
AssetType.POINTCLOUD: "georeferenced_model.laz",
|
||||
AssetType.TEXTURED_MODEL: "textured_model.zip",
|
||||
}
|
||||
|
||||
FILE_TO_ASSET = dict([reversed(i) for i in ASSET_TO_FILE.items()])
|
||||
|
||||
ASSET_TO_OUTPUT = {
|
||||
AssetType.ORTHOPHOTO: OutputType.IMAGERY,
|
||||
AssetType.TERRAIN_MODEL: OutputType.TERRAIN,
|
||||
AssetType.SURFACE_MODEL: OutputType.TERRAIN,
|
||||
AssetType.POINTCLOUD: OutputType.TILES,
|
||||
AssetType.TEXTURED_MODEL: OutputType.TILES,
|
||||
}
|
||||
|
||||
ASSET_TO_SOURCE = {
|
||||
AssetType.ORTHOPHOTO: SourceType.RASTER_IMAGERY,
|
||||
AssetType.TERRAIN_MODEL: SourceType.RASTER_TERRAIN,
|
||||
AssetType.SURFACE_MODEL: SourceType.RASTER_TERRAIN,
|
||||
AssetType.POINTCLOUD: SourceType.POINTCLOUD,
|
||||
AssetType.TEXTURED_MODEL: SourceType.CAPTURE,
|
||||
}
|
||||
|
||||
### ###
|
||||
# RECIEVERS #
|
||||
### ###
|
||||
@receiver(plugin_signals.task_removed, dispatch_uid="oam_on_task_removed")
|
||||
@receiver(plugin_signals.task_completed, dispatch_uid="oam_on_task_completed")
|
||||
def oam_cleanup(sender, task_id, **kwargs):
|
||||
# When a task is removed, simply remove clutter
|
||||
# When a task is re-processed, make sure we can re-share it if we shared a task previously
|
||||
for asset_type in AssetType:
|
||||
del_asset_info(task_id, asset_type)
|
||||
|
||||
|
||||
### ###
|
||||
# API VIEWS #
|
||||
### ###
|
||||
class EnumField(ChoiceField):
|
||||
default_error_messages = {"invalid": _("No matching enum type.")}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.enum_type = kwargs.pop("enum_type")
|
||||
choices = [enum_item.value for enum_item in self.enum_type]
|
||||
self.choice_set = set(choices)
|
||||
super().__init__(choices, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data in self.choice_set:
|
||||
return self.enum_type[data]
|
||||
self.fail("invalid")
|
||||
|
||||
def to_representation(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return value.value
|
||||
|
||||
|
||||
class UploadSerializer(serializers.Serializer):
|
||||
token = CharField()
|
||||
name = CharField()
|
||||
asset_type = EnumField(enum_type=AssetType)
|
||||
description = CharField(default="", required=False, allow_blank=True)
|
||||
attribution = CharField(default="", required=False, allow_blank=True)
|
||||
options = JSONField(default={}, required=False)
|
||||
|
||||
|
||||
class UpdateIonAssets(serializers.Serializer):
|
||||
token = CharField()
|
||||
|
||||
|
||||
class ShareTaskView(TaskView):
|
||||
def get(self, request, pk=None):
|
||||
task = self.get_and_check_task(request, pk)
|
||||
|
||||
assets = []
|
||||
for file_name in task.available_assets:
|
||||
if file_name not in FILE_TO_ASSET:
|
||||
continue
|
||||
asset_type = FILE_TO_ASSET[file_name]
|
||||
|
||||
asset_info = get_asset_info(task.id, asset_type)
|
||||
ion_id = asset_info["id"]
|
||||
is_error = len(asset_info["error"]) > 0
|
||||
is_task = is_asset_task(asset_info)
|
||||
is_exported = asset_info["id"] is not None and not is_task
|
||||
|
||||
assets.append(
|
||||
{
|
||||
"type": asset_type,
|
||||
"isError": is_error,
|
||||
"isTask": is_task,
|
||||
"isExported": is_exported,
|
||||
**asset_info,
|
||||
}
|
||||
)
|
||||
|
||||
return Response({"items": assets}, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request, pk=None):
|
||||
from app.plugins import logger
|
||||
task = self.get_and_check_task(request, pk)
|
||||
serializer = UploadSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
token, asset_type, name, description, attribution, options = pluck(
|
||||
serializer.validated_data,
|
||||
"token",
|
||||
"asset_type",
|
||||
"name",
|
||||
"description",
|
||||
"attribution",
|
||||
"options",
|
||||
)
|
||||
asset_path = task.get_asset_download_path(ASSET_TO_FILE[asset_type])
|
||||
|
||||
# Skip already processing tasks
|
||||
if asset_type not in get_processing_assets(task.id):
|
||||
if asset_type == AssetType.TEXTURED_MODEL and "position" not in options:
|
||||
value = task.ASSETS_MAP[ASSET_TO_FILE[asset_type]]
|
||||
if isinstance(value, dict):
|
||||
if 'deferred_compress_dir' in value:
|
||||
odm_path = value['deferred_compress_dir']
|
||||
asset_path = task.generate_deferred_asset(asset_path, odm_path, False)
|
||||
logger.info(f"generate_deferred_asset at {asset_path}")
|
||||
|
||||
extent = None
|
||||
if task.dsm_extent is not None:
|
||||
extent = task.dsm_extent.extent
|
||||
if task.dtm_extent is not None:
|
||||
extent = task.dtm_extent.extent
|
||||
if extent is None:
|
||||
print(f"Unable to find task boundary: {task}")
|
||||
else:
|
||||
lng, lat = extent[0], extent[1]
|
||||
# height is set to zero as model is already offset
|
||||
options["position"] = [lng, lat, 0]
|
||||
|
||||
del_asset_info(task.id, asset_type)
|
||||
asset_info = get_asset_info(task.id, asset_type)
|
||||
asset_info["upload"]["active"] = True
|
||||
set_asset_info(task.id, asset_type, asset_info)
|
||||
|
||||
run_function_async(upload_to_ion,
|
||||
task.id,
|
||||
asset_type,
|
||||
token,
|
||||
asset_path,
|
||||
name,
|
||||
description,
|
||||
attribution,
|
||||
options,
|
||||
)
|
||||
else:
|
||||
print(f"Ignore running ion task {task.id} {str(asset_type)}")
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class RefreshIonTaskView(TaskView):
|
||||
def post(self, request, pk=None):
|
||||
serializer = UpdateIonAssets(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
task = self.get_and_check_task(request, pk)
|
||||
token = serializer.validated_data["token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
is_updated = False
|
||||
# ion cleanup check
|
||||
for asset_type in AssetType:
|
||||
asset_info = get_asset_info(task.id, asset_type)
|
||||
ion_id = asset_info["id"]
|
||||
if ion_id is None:
|
||||
continue
|
||||
res = requests.get(f"{ION_API_URL}/assets/{ion_id}", headers=headers)
|
||||
if res.status_code != 200:
|
||||
del_asset_info(task.id, asset_type)
|
||||
is_updated = True
|
||||
|
||||
# dead task cleanup
|
||||
for asset_type in get_processing_assets(task.id):
|
||||
del_asset_info(task.id, asset_type)
|
||||
is_updated = True
|
||||
|
||||
return Response({"updated": is_updated}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ClearErrorsTaskView(TaskView):
|
||||
def post(self, request, pk=None):
|
||||
task = self.get_and_check_task(request, pk)
|
||||
for asset_type in AssetType:
|
||||
asset_info = get_asset_info(task.id, asset_type)
|
||||
if len(asset_info["error"]) <= 0:
|
||||
continue
|
||||
del_asset_info(task.id, asset_type)
|
||||
|
||||
return Response({"complete": True}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import json
|
||||
import requests
|
||||
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from .globals import ION_API_URL
|
||||
|
||||
|
||||
class TokenForm(forms.Form):
|
||||
token = forms.CharField(
|
||||
label="",
|
||||
required=False,
|
||||
max_length=1024,
|
||||
widget=forms.TextInput(attrs={"placeholder": "Token"}),
|
||||
)
|
||||
|
||||
|
||||
def JsonResponse(dictionary):
|
||||
return HttpResponse(json.dumps(dictionary), content_type="application/json")
|
||||
|
||||
|
||||
def HomeView(plugin):
|
||||
@login_required
|
||||
def view(request):
|
||||
ds = plugin.get_user_data_store(request.user)
|
||||
|
||||
# if this is a POST request we need to process the form data
|
||||
if request.method == "POST":
|
||||
form = TokenForm(request.POST)
|
||||
if form.is_valid():
|
||||
token = form.cleaned_data["token"].strip()
|
||||
if len(token) > 0:
|
||||
messages.success(request, "Updated Cesium ion Token!")
|
||||
else:
|
||||
messages.info(request, "Reset Cesium ion Token")
|
||||
ds.set_string("token", token)
|
||||
|
||||
form = TokenForm(initial={"token": ds.get_string("token", default="")})
|
||||
|
||||
return render(
|
||||
request,
|
||||
plugin.template_path("app.html"),
|
||||
{"title": "Cesium Ion", "form": form},
|
||||
)
|
||||
|
||||
return view
|
||||
|
||||
|
||||
def LoadButtonView(plugin):
|
||||
@login_required
|
||||
def view(request):
|
||||
ds = plugin.get_user_data_store(request.user)
|
||||
token = ds.get_string("token")
|
||||
|
||||
return render(
|
||||
request,
|
||||
plugin.template_path("load_buttons.js"),
|
||||
{
|
||||
"token": token,
|
||||
"app_name": plugin.get_name(),
|
||||
"api_url": plugin.public_url("").rstrip("/"),
|
||||
"ion_url": ION_API_URL,
|
||||
},
|
||||
content_type="text/javascript",
|
||||
)
|
||||
|
||||
return view
|
|
@ -0,0 +1,2 @@
|
|||
PROJECT_NAME = __name__.split(".")[-2]
|
||||
ION_API_URL = "https://api.cesium.com/v1"
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Cesium Ion",
|
||||
"webodmMinVersion": "2.5.0",
|
||||
"description": "Upload and tile ODM assets with Cesium ion.",
|
||||
"version": "1.3.0",
|
||||
"author": "Cesium GS, Inc",
|
||||
"email": "hello@cesium.com",
|
||||
"repository": " https://github.com/OpenDroneMap/WebODM",
|
||||
"tags": ["cesium ion", "cesium"],
|
||||
"homepage": "https://cesium.com",
|
||||
"experimental": false,
|
||||
"deprecated": false
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
from os import path, mkdir, walk, remove as removeFile
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp
|
||||
|
||||
|
||||
DELETE_EXTENSIONS = (".conf", ".vec", ".spt")
|
||||
OBJ_FILE_EXTENSION = ".obj"
|
||||
MTL_FILE_EXTENSION = ".mtl"
|
||||
|
||||
|
||||
class IonInvalidZip(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def file_walk(directory):
|
||||
for root, _, file_names in walk(directory):
|
||||
for file_name in file_names:
|
||||
yield path.join(root, file_name)
|
||||
|
||||
|
||||
def zip_dir(zip_name, directory, destructive=False):
|
||||
with ZipFile(zip_name, mode="w", compression=ZIP_DEFLATED) as zipfile:
|
||||
for file_path in file_walk(directory):
|
||||
relpath = path.relpath(file_path, directory)
|
||||
zipfile.write(file_path, relpath)
|
||||
if destructive:
|
||||
removeFile(file_path)
|
||||
|
||||
|
||||
def to_ion_texture_model(texture_model_path, dest_directory=None, minimize_space=True):
|
||||
is_tmp = False
|
||||
if dest_directory is None:
|
||||
is_tmp = True
|
||||
dest_directory = mkdtemp()
|
||||
dest_file = path.join(dest_directory, path.basename(texture_model_path))
|
||||
try:
|
||||
unzip_dir = path.join(dest_directory, "_tmp")
|
||||
mkdir(unzip_dir)
|
||||
with ZipFile(texture_model_path) as zipfile:
|
||||
zipfile.extractall(unzip_dir)
|
||||
|
||||
files_to_delete = set()
|
||||
found_geo = False
|
||||
for file_name in file_walk(unzip_dir):
|
||||
if file_name.endswith(DELETE_EXTENSIONS):
|
||||
files_to_delete.add(file_name)
|
||||
elif file_name.endswith(".obj"):
|
||||
if "_geo" in path.basename(file_name):
|
||||
found_geo = True
|
||||
else:
|
||||
file_name = path.splitext(file_name)[0]
|
||||
files_to_delete.add(file_name + OBJ_FILE_EXTENSION)
|
||||
files_to_delete.add(file_name + MTL_FILE_EXTENSION)
|
||||
|
||||
if not found_geo:
|
||||
raise IonInvalidZip("Unable to find geo file")
|
||||
|
||||
for file_name in files_to_delete:
|
||||
if not path.isfile(file_name):
|
||||
continue
|
||||
removeFile(file_name)
|
||||
|
||||
zip_dir(dest_file, unzip_dir, destructive=minimize_space)
|
||||
rmtree(unzip_dir)
|
||||
except Exception as e:
|
||||
if is_tmp:
|
||||
rmtree(dest_directory)
|
||||
raise e
|
||||
|
||||
return dest_file, dest_directory
|
|
@ -0,0 +1,39 @@
|
|||
import re
|
||||
import json
|
||||
|
||||
from app.plugins import PluginBase, Menu, MountPoint, logger
|
||||
|
||||
from .globals import PROJECT_NAME
|
||||
from .api_views import ShareTaskView, RefreshIonTaskView, ClearErrorsTaskView
|
||||
from .app_views import HomeView, LoadButtonView
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.name = PROJECT_NAME
|
||||
|
||||
def main_menu(self):
|
||||
return [Menu("Cesium Ion", self.public_url(""), "fa-cesium fa fa-fw")]
|
||||
|
||||
def include_js_files(self):
|
||||
return ["load_buttons.js"]
|
||||
|
||||
def include_css_files(self):
|
||||
return ["font.css", "build/TaskView.css"]
|
||||
|
||||
def build_jsx_components(self):
|
||||
return ["TaskView.jsx"]
|
||||
|
||||
def api_mount_points(self):
|
||||
return [
|
||||
MountPoint("task/(?P<pk>[^/.]+)/share", ShareTaskView.as_view()),
|
||||
MountPoint("task/(?P<pk>[^/.]+)/refresh", RefreshIonTaskView.as_view()),
|
||||
MountPoint("task/(?P<pk>[^/.]+)/clear", ClearErrorsTaskView.as_view()),
|
||||
]
|
||||
|
||||
def app_mount_points(self):
|
||||
return [
|
||||
MountPoint("$", HomeView(self)),
|
||||
MountPoint("load_buttons.js$", LoadButtonView(self)),
|
||||
]
|
|
@ -0,0 +1,291 @@
|
|||
import React, { Component, Fragment } from "react";
|
||||
|
||||
import ErrorMessage from "webodm/components/ErrorMessage";
|
||||
|
||||
import IonAssetButton from "./components/IonAssetButton";
|
||||
import UploadDialog from "./components/UploadDialog";
|
||||
import TasksDialog from "./components/TasksDialog";
|
||||
import AppContext from "./components/AppContext";
|
||||
import {
|
||||
ImplicitTaskFetcher as TaskFetcher,
|
||||
APIFetcher
|
||||
} from "./components/Fetcher";
|
||||
import {AssetConfig, AssetStyles} from "./defaults";
|
||||
import { fetchCancelable, getCookie } from "./utils";
|
||||
|
||||
export default class TaskView extends Component {
|
||||
constructor(){
|
||||
super();
|
||||
this.onOpenUploadDialog = this.onOpenUploadDialog.bind(this);
|
||||
this.showTaskDialog = this.showTaskDialog.bind(this);
|
||||
}
|
||||
|
||||
state = {
|
||||
error: "",
|
||||
currentAsset: null,
|
||||
isTasksDialog: false,
|
||||
isUploadDialogLoading: false
|
||||
};
|
||||
|
||||
cancelableFetch = null;
|
||||
timeoutHandler = null;
|
||||
refreshAssets = null;
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.timeoutHandler !== null) {
|
||||
clearTimeout(this.timeoutHandler);
|
||||
this.timeoutHandler = null;
|
||||
}
|
||||
if (this.cancelableFetch !== null) {
|
||||
try {
|
||||
this.cancelableFetch.cancel();
|
||||
this.cancelableFetch = null;
|
||||
} catch (exception) {
|
||||
console.error(exception);
|
||||
}
|
||||
}
|
||||
|
||||
this.refreshAssets = null;
|
||||
}
|
||||
|
||||
onOpenUploadDialog(asset) {
|
||||
this.setState({ currentAsset: asset });
|
||||
if (this.uploadDialog != null)
|
||||
{
|
||||
this.uploadDialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
onHideUploadDialog = () =>
|
||||
this.setState({ currentAsset: null, isUploadDialogLoading: false });
|
||||
|
||||
showTaskDialog() {
|
||||
this.setState({ isTasksDialog: true });
|
||||
if (this.tasksDialog != null)
|
||||
{
|
||||
this.tasksDialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
hideTaskDialog = () => this.setState({ isTasksDialog: false });
|
||||
|
||||
onUploadAsset = async data => {
|
||||
const { task, token, apiURL } = this.props;
|
||||
const { currentAsset } = this.state;
|
||||
const payload = {...data};
|
||||
|
||||
if (currentAsset === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
payload.token = token;
|
||||
payload.asset_type = currentAsset;
|
||||
|
||||
this.setState({ isUploadDialogLoading: true });
|
||||
this.cancelableFetch = await fetchCancelable(
|
||||
`/api${apiURL}/task/${task.id}/share`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
)
|
||||
.promise.then(this.refreshAssets)
|
||||
.finally(this.onHideUploadDialog);
|
||||
};
|
||||
|
||||
onClearFailedAssets = () => {
|
||||
const { task, apiURL } = this.props;
|
||||
|
||||
this.cancelableFetch = fetchCancelable(
|
||||
`/api${apiURL}/task/${task.id}/clear`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken")
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.cancelableFetch.promise.then(this.refreshAssets);
|
||||
};
|
||||
|
||||
onAssetsRefreshed = ({ items = [] }) => {
|
||||
const { isTasksDialog } = this.state;
|
||||
const hasTasks = items.some(item => item.isTask);
|
||||
|
||||
if (! hasTasks){
|
||||
this.hideTaskDialog();
|
||||
}
|
||||
|
||||
if (items.some(item => item.isTask && !item.isError)) {
|
||||
const timeout = 4000 / (isTasksDialog ? 2 : 1);
|
||||
this.timeoutHandler = setTimeout(this.refreshAssets, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
onCleanStatus = ({ updated = false }) => {
|
||||
if (! updated || this.refreshAssets == null){
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshAssets();
|
||||
};
|
||||
|
||||
onErrorUploadDialog = msg => {
|
||||
this.setState({ error: msg });
|
||||
this.onHideUploadDialog();
|
||||
};
|
||||
|
||||
handleAssetSelect = data => asset => {
|
||||
const idMap = data.items
|
||||
.filter(item => item.isExported)
|
||||
.reduce((accum, item) => {
|
||||
accum[item.type] = item.id;
|
||||
return accum;
|
||||
}, {});
|
||||
|
||||
if (idMap[asset] === undefined) {
|
||||
console.warn('Asset not found.', asset);
|
||||
}
|
||||
|
||||
window.open(`https://cesium.com/ion/assets/${idMap[asset] ?? ''}`);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { task, token } = this.props;
|
||||
const {
|
||||
isTasksDialog,
|
||||
isUploadDialogLoading,
|
||||
currentAsset
|
||||
} = this.state;
|
||||
const isUploadDialog = currentAsset !== null;
|
||||
const assetName = isUploadDialog ? AssetConfig[currentAsset].name : "";
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={this.props}>
|
||||
<ErrorMessage bind={[this, "error"]} />
|
||||
<div className={"ion-dropdowns"}>
|
||||
<TaskFetcher
|
||||
path={"share"}
|
||||
onLoad={this.onAssetsRefreshed}
|
||||
onBindRefresh={method => (this.refreshAssets = method)}
|
||||
>
|
||||
{({ isError, data = {} }) => {
|
||||
// Asset Export and View Selector
|
||||
const { items = [] } = data;
|
||||
const available = items
|
||||
.filter(
|
||||
item => {
|
||||
return (! item.isExported && ! item.isTask) || item.isError
|
||||
}
|
||||
)
|
||||
.map(item => item.type);
|
||||
|
||||
const exported = items
|
||||
.filter(item => item.isExported)
|
||||
.map(item => item.type);
|
||||
|
||||
// Tasks Selector
|
||||
const processing = items.filter(item => item.isTask);
|
||||
const hasProcessingTasks = processing.length > 0;
|
||||
const hasErrors = processing.some(item => item.isError);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{available.length > 0 && (
|
||||
<IonAssetButton
|
||||
assets={available}
|
||||
onSelect={this.onOpenUploadDialog}
|
||||
>
|
||||
Tile in Cesium Ion
|
||||
</IonAssetButton>
|
||||
)}
|
||||
|
||||
{exported.length > 0 && (
|
||||
<IonAssetButton
|
||||
assets={exported}
|
||||
onSelect={this.handleAssetSelect(
|
||||
data
|
||||
)}
|
||||
>
|
||||
View in Cesium Ion
|
||||
</IonAssetButton>
|
||||
)}
|
||||
{items.length <= 0 && (
|
||||
<button
|
||||
className={"ion-btn btn btn-primary btn-sm"}
|
||||
onClick={this.refreshAssets}
|
||||
>
|
||||
<i className={"fa fa-cesium"} />
|
||||
Refresh Available ion Assets
|
||||
</button>
|
||||
)}
|
||||
{hasProcessingTasks && (
|
||||
<button
|
||||
className={`ion-btn btn btn-sm ${hasErrors ? "btn-danger" : "btn-primary"}`}
|
||||
onClick={this.showTaskDialog}
|
||||
>
|
||||
<i className={"fa fa-cesium"} />
|
||||
View ion Tasks
|
||||
</button>
|
||||
)}
|
||||
<TasksDialog
|
||||
title={"Cesium ion Tasks"}
|
||||
show={isTasksDialog}
|
||||
tasks={processing}
|
||||
onHide={this.hideTaskDialog}
|
||||
onClearFailed={this.onClearFailedAssets}
|
||||
ref={(domNode) => { this.tasksDialog = domNode; }}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}}
|
||||
</TaskFetcher>
|
||||
</div>
|
||||
|
||||
<APIFetcher path={"projects/"} params={{ id: task.project }}>
|
||||
{({ isLoading, isError, data }) => {
|
||||
const initialValues = {};
|
||||
|
||||
if (isLoading || assetName === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isLoading && !isError && data?.length > 0) {
|
||||
const project = data[0];
|
||||
initialValues.name = `${project.name} | ${task.name} — ${assetName}`;
|
||||
initialValues.description = project.description;
|
||||
}
|
||||
|
||||
return (
|
||||
<UploadDialog
|
||||
title={`Tile in Cesium ion — ${assetName}`}
|
||||
initialValues={initialValues}
|
||||
show={isUploadDialog}
|
||||
loading={isUploadDialogLoading}
|
||||
asset={currentAsset}
|
||||
onHide={this.onHideUploadDialog}
|
||||
onSubmit={this.onUploadAsset}
|
||||
onError={this.onErrorUploadDialog}
|
||||
ref={(domNode) => { this.uploadDialog = domNode; }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</APIFetcher>
|
||||
<TaskFetcher
|
||||
method={"POST"}
|
||||
path={"refresh"}
|
||||
body={JSON.stringify({ token })}
|
||||
onLoad={this.onCleanStatus}
|
||||
/>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
const AppContext = React.createContext({
|
||||
apiUrl: null,
|
||||
ionURL: null,
|
||||
token: null,
|
||||
task: null
|
||||
});
|
||||
|
||||
export default AppContext;
|
|
@ -0,0 +1,140 @@
|
|||
import React, { PureComponent } from "react";
|
||||
|
||||
import AppContext from "./AppContext";
|
||||
import { fetchCancelable, getCookie } from "../utils";
|
||||
|
||||
export class Fetcher extends PureComponent {
|
||||
static defaultProps = {
|
||||
url: "",
|
||||
path: "",
|
||||
method: "GET",
|
||||
onBindRefresh: () => {},
|
||||
onError: () => {},
|
||||
onLoad: () => {}
|
||||
};
|
||||
|
||||
state = {
|
||||
isLoading: true,
|
||||
isError: false
|
||||
};
|
||||
|
||||
cancelableFetch = null;
|
||||
|
||||
fetch = () => {
|
||||
const {
|
||||
url,
|
||||
path,
|
||||
onError,
|
||||
onLoad,
|
||||
refresh,
|
||||
children,
|
||||
params,
|
||||
...options
|
||||
} = this.props;
|
||||
|
||||
let queryURL = `${url}/${path}`;
|
||||
if (params !== undefined) {
|
||||
const serializedParams = `?${Object.keys(params)
|
||||
.map(key =>
|
||||
[key, params[key]].map(encodeURIComponent).join("=")
|
||||
)
|
||||
.join("&")}`;
|
||||
queryURL = queryURL.replace(/[\/\?]+$/, "");
|
||||
queryURL += serializedParams;
|
||||
}
|
||||
|
||||
this.cancelableFetch = fetchCancelable(queryURL, options);
|
||||
return this.cancelableFetch.promise
|
||||
.then(res => {
|
||||
if (res.status !== 200) throw new Error(res.status);
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
this.setState({ data, isLoading: false });
|
||||
onLoad(data);
|
||||
})
|
||||
.catch(out => {
|
||||
if (out.isCanceled) return;
|
||||
this.setState({ error: out, isLoading: false, isError: true });
|
||||
onError(out);
|
||||
})
|
||||
.finally(() => (this.cancelableFetch = null));
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.fetch();
|
||||
this.props.onBindRefresh(this.fetch);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.onBindRefresh(null);
|
||||
if (this.cancelableFetch === null) return;
|
||||
this.cancelableFetch.cancel();
|
||||
this.cancelableFetch = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
if (children == null) return null;
|
||||
if (typeof children !== "function")
|
||||
return React.cloneElement(children, this.state);
|
||||
else return children(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
const ImplicitFetcher = ({
|
||||
url,
|
||||
getURL = null,
|
||||
getOptions = null,
|
||||
...options
|
||||
}) => (
|
||||
<AppContext.Consumer>
|
||||
{context => (
|
||||
<Fetcher
|
||||
url={getURL !== null ? getURL(context, options) : url}
|
||||
{...(getOptions !== null ? getOptions(context, options) : {})}
|
||||
{...options}
|
||||
/>
|
||||
)}
|
||||
</AppContext.Consumer>
|
||||
);
|
||||
|
||||
const APIFetcher = props => (
|
||||
<Fetcher
|
||||
url={"/api"}
|
||||
credentials={"same-origin"}
|
||||
headers={{
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const ImplicitTaskFetcher = props => (
|
||||
<ImplicitFetcher
|
||||
getURL={({ apiURL, task }) => `/api${apiURL}/task/${task.id}`}
|
||||
credentials={"same-origin"}
|
||||
headers={{
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const ImplicitIonFetcher = props => (
|
||||
<ImplicitFetcher
|
||||
getURL={({ ionURL }) => ionURL}
|
||||
getOptions={({ token }) => ({
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export { APIFetcher, ImplicitTaskFetcher, ImplicitIonFetcher };
|
|
@ -0,0 +1,35 @@
|
|||
import React from "react";
|
||||
import { connect } from "formik";
|
||||
|
||||
class FormikErrorFocus extends React.Component {
|
||||
isObject(value) {
|
||||
return (
|
||||
value && typeof value === "object" && value.constructor === Object
|
||||
);
|
||||
}
|
||||
getKeysRecursively = object => {
|
||||
if (!this.isObject(object)) {
|
||||
return "";
|
||||
}
|
||||
const currentKey = Object.keys(object)[0];
|
||||
if (!this.getKeysRecursively(object[currentKey])) {
|
||||
return currentKey;
|
||||
}
|
||||
return currentKey + "." + this.getKeysRecursively(object[currentKey]);
|
||||
};
|
||||
componentDidUpdate(prevProps) {
|
||||
const { isSubmitting, isValidating, errors } = prevProps.formik;
|
||||
const keys = Object.keys(errors);
|
||||
if (keys.length > 0 && isSubmitting && !isValidating) {
|
||||
const selectorKey = this.getKeysRecursively(errors);
|
||||
const selector = `[id="${selectorKey}"], [name="${selectorKey}"] `;
|
||||
const errorElement = document.querySelector(selector);
|
||||
if (errorElement) errorElement.focus();
|
||||
console.warn(errors);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export default connect(FormikErrorFocus);
|
|
@ -0,0 +1,55 @@
|
|||
import React, { PureComponent, Fragment } from "react";
|
||||
import IonAssetLabel from "./IonAssetLabel";
|
||||
import { AssetStyles } from "../defaults";
|
||||
|
||||
import "./IonAssetButton.scss";
|
||||
|
||||
export default class IonAssetButton extends PureComponent {
|
||||
static defaultProps = {
|
||||
assets: [],
|
||||
assetComponent: IonAssetLabel,
|
||||
onSelect: () => {}
|
||||
};
|
||||
|
||||
handleClick = asset => () => this.props.onSelect(asset);
|
||||
|
||||
render() {
|
||||
const {
|
||||
assets,
|
||||
onSelect,
|
||||
children,
|
||||
assetComponent: AssetComponent
|
||||
} = this.props;
|
||||
|
||||
const menuItems = assets
|
||||
.sort((a, b) => AssetStyles[a].name.localeCompare(AssetStyles[b].name))
|
||||
.map(asset => (
|
||||
<li>
|
||||
<a key={asset} style={{cursor:'pointer'}} onClick={this.handleClick(asset)}>
|
||||
<AssetComponent asset={asset} showIcon={true} />
|
||||
</a>
|
||||
</li>
|
||||
));
|
||||
|
||||
const title = (
|
||||
<Fragment>
|
||||
<i className={"fa fa-cesium"} />
|
||||
{children}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"btn-group"}>
|
||||
<button type="button" className={"btn btn-sm btn-primary"} data-toggle="dropdown">
|
||||
{title}
|
||||
</button>
|
||||
<button type="button" className={"btn btn-sm dropdown-toggle btn-primary"} data-toggle="dropdown">
|
||||
<span className="caret"></span>
|
||||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
{menuItems}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
.ion-btn {
|
||||
.fa-cesium {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.caret {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.ion-dropdowns .dropdown {
|
||||
float: none;
|
||||
margin-right: 4px;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import React, { PureComponent, Fragment } from "react";
|
||||
import { AssetStyles } from "../defaults";
|
||||
|
||||
const IonAssetLabel = ({ asset, showIcon = false, ...options }) => (
|
||||
<Fragment>
|
||||
{showIcon && <i className={`${AssetStyles[asset].icon}`} />}
|
||||
{" "}
|
||||
{AssetStyles[asset].name}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export default IonAssetLabel;
|
|
@ -0,0 +1,123 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const IonFieldComponent = ({
|
||||
name,
|
||||
value,
|
||||
label,
|
||||
help,
|
||||
type = "text",
|
||||
showIcon = true,
|
||||
error,
|
||||
touched,
|
||||
onChange,
|
||||
onBlur,
|
||||
...props
|
||||
}) => {
|
||||
const isError = error && touched;
|
||||
const isCheckbox = type === "checkbox";
|
||||
const ControlComponent = isCheckbox ? "input" : (type === "textarea" || type === "select") ? type : "input";
|
||||
|
||||
return (
|
||||
<div className={`form-group${isError ? ' has-error' : ''}`} style={{ marginLeft: 0, marginRight: 0 }}>
|
||||
{label && !isCheckbox && <label htmlFor={name} className="control-label">{label}</label>}
|
||||
<ControlComponent
|
||||
id={name}
|
||||
name={name}
|
||||
className={isCheckbox ? "" : "form-control"}
|
||||
type={type}
|
||||
value={isCheckbox ? undefined : value}
|
||||
checked={isCheckbox ? value : undefined}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
{...props}
|
||||
/>
|
||||
{label && isCheckbox && <label htmlFor={name} className="control-label">{label}</label>}
|
||||
{isError && <span className="help-block">{error}</span>}
|
||||
{help && !isError && <span className="help-block">{help}</span>}
|
||||
{isError && showIcon && <span className="glyphicon glyphicon-remove form-control-feedback"></span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
IonFieldComponent.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
label: PropTypes.string,
|
||||
help: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
showIcon: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
touched: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBlur: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class IonField extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: props.type === "checkbox" ? props.checked : props.value || '',
|
||||
touched: false,
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
const { type, checked, value } = e.target;
|
||||
const newValue = type === "checkbox" ? checked : value;
|
||||
this.setState({ value: newValue }, () => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleBlur = (e) => {
|
||||
this.setState({ touched: true }, () => {
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { name, label, help, type, showIcon, validate, ...props } = this.props;
|
||||
const { value, touched, error } = this.state;
|
||||
|
||||
let validationError = error;
|
||||
if (validate) {
|
||||
validationError = validate(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<IonFieldComponent
|
||||
name={name}
|
||||
value={value}
|
||||
label={label}
|
||||
help={help}
|
||||
type={type}
|
||||
showIcon={showIcon}
|
||||
error={validationError}
|
||||
touched={touched}
|
||||
onChange={this.handleChange}
|
||||
onBlur={this.handleBlur}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IonField.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
help: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
showIcon: PropTypes.bool,
|
||||
validate: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onBlur: PropTypes.func
|
||||
};
|
||||
|
||||
export { IonFieldComponent };
|
||||
export default IonField;
|
|
@ -0,0 +1,19 @@
|
|||
.ion-tasks .list-group .list-group-item {
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
border-radius: 0;
|
||||
padding: 20px 0;
|
||||
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
// import ErrorMessage from './ErrorMessage';
|
||||
import IonAssetLabel from './IonAssetLabel';
|
||||
import './TaskDialog.scss';
|
||||
import $ from 'jquery';
|
||||
|
||||
const TaskStatusItem = ({
|
||||
asset,
|
||||
progress,
|
||||
task,
|
||||
helpText = '',
|
||||
active = true,
|
||||
bsStyle = 'primary'
|
||||
}) => (
|
||||
<div className="list-group-item">
|
||||
<div className="row">
|
||||
<div className="col-xs-6">
|
||||
<p style={{ fontWeight: 'bold' }}>
|
||||
<IonAssetLabel asset={asset} showIcon={true} />
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-xs-6">
|
||||
<p className="pull-right">Status: {task}</p>
|
||||
</div>
|
||||
</div>
|
||||
<progress value={progress} max="100" className={bsStyle}></progress>
|
||||
{helpText && <small>{helpText}</small>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default class TaskDialog extends React.Component {
|
||||
static defaultProps = {
|
||||
tasks: [],
|
||||
taskComponent: TaskStatusItem,
|
||||
show: false
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
tasks: PropTypes.array.isRequired,
|
||||
taskComponent: PropTypes.elementType,
|
||||
onClearFailed: PropTypes.func.isRequired,
|
||||
onHide: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showModal: props.show,
|
||||
error: ''
|
||||
};
|
||||
|
||||
this.setModal = this.setModal.bind(this);
|
||||
this.show = this.show.bind(this);
|
||||
this.hide = this.hide.bind(this);
|
||||
}
|
||||
|
||||
setModal(domNode) {
|
||||
this.modal = domNode;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._mounted = true;
|
||||
|
||||
$(this.modal)
|
||||
.on('hidden.bs.modal', () => {
|
||||
this.hide();
|
||||
})
|
||||
.on('shown.bs.modal', () => {
|
||||
if (this.props.onShow) this.props.onShow();
|
||||
});
|
||||
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._mounted = false;
|
||||
$(this.modal).off('hidden.bs.modal shown.bs.modal').modal('hide');
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.showModal) {
|
||||
$(this.modal).modal('show');
|
||||
} else {
|
||||
$(this.modal).modal('hide');
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
this.setState({ showModal: true, error: '' });
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.setState({ showModal: false });
|
||||
if (this.props.onHide) this.props.onHide();
|
||||
}
|
||||
|
||||
renderTaskItems() {
|
||||
const { tasks, taskComponent: TaskComponent } = this.props;
|
||||
let hasErrors = false;
|
||||
|
||||
const taskItems = tasks.map(
|
||||
({ type: asset, upload, process, error }) => {
|
||||
let task,
|
||||
style,
|
||||
progress = 0;
|
||||
|
||||
if (upload.active) {
|
||||
progress = upload.progress;
|
||||
task = 'Uploading';
|
||||
style = 'info';
|
||||
} else if (process.active) {
|
||||
progress = process.progress;
|
||||
task = 'Processing';
|
||||
style = 'success';
|
||||
}
|
||||
|
||||
if (error.length > 0) {
|
||||
task = 'Error';
|
||||
style = 'danger';
|
||||
console.error(error);
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<TaskComponent
|
||||
key={asset}
|
||||
asset={asset}
|
||||
progress={progress * 100}
|
||||
task={task}
|
||||
bsStyle={style}
|
||||
helpText={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return { taskItems, hasErrors };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onClearFailed } = this.props;
|
||||
const { taskItems, hasErrors } = this.renderTaskItems();
|
||||
|
||||
return (
|
||||
<div ref={this.setModal} className="modal task-dialog" tabIndex="-1" data-backdrop="static">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" onClick={this.hide}>
|
||||
<span>×</span>
|
||||
</button>
|
||||
<h4 className="modal-title">
|
||||
<i className="fa fa-cesium" /> Cesium Ion Tasks
|
||||
</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{/* <ErrorMessage bind={[this, "error"]} /> */}
|
||||
<div className="list-group">{taskItems}</div>
|
||||
|
||||
{hasErrors && (
|
||||
<button
|
||||
className="center-block btn btn-danger btn-sm"
|
||||
onClick={onClearFailed}
|
||||
>
|
||||
<i className="glyphicon glyphicon-trash"></i>
|
||||
Remove Failed Tasks
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-primary" onClick={this.hide}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
import React, { Component, Fragment } from "react";
|
||||
|
||||
import FormDialog from "../../../../app/static/app/js/components/FormDialog";
|
||||
|
||||
import IonField from "./IonField";
|
||||
import { ImplicitIonFetcher as IonFetcher } from "./Fetcher";
|
||||
import { AssetType, SourceType } from "../defaults";
|
||||
import "./UploadDialog.scss";
|
||||
|
||||
export default class UploadDialog extends Component {
|
||||
static AssetSourceType = {
|
||||
[AssetType.ORTHOPHOTO]: SourceType.RASTER_IMAGERY,
|
||||
[AssetType.TERRAIN_MODEL]: SourceType.RASTER_TERRAIN,
|
||||
[AssetType.SURFACE_MODEL]: SourceType.RASTER_TERRAIN,
|
||||
[AssetType.POINTCLOUD]: SourceType.POINTCLOUD,
|
||||
[AssetType.TEXTURED_MODEL]: SourceType.CAPTURE
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
show: true,
|
||||
asset: null,
|
||||
loading: false,
|
||||
initialValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
attribution: "",
|
||||
options: {
|
||||
baseTerrainId: "",
|
||||
textureFormat: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.mergedInitialValues = {
|
||||
...UploadDialog.defaultProps.initialValues,
|
||||
...this.props.initialValues
|
||||
};
|
||||
|
||||
this.state = {
|
||||
title : props.title,
|
||||
...this.mergedInitialValues
|
||||
}
|
||||
}
|
||||
|
||||
show(){
|
||||
this.dialog.show();
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
const { value, name } = e.target;
|
||||
|
||||
if (name === "options.textureFormat")
|
||||
{
|
||||
let options = {...this.state.options};
|
||||
options["textureFormat"] = value === "Yes";
|
||||
this.setState({ options });
|
||||
}
|
||||
else if (name === "options.baseTerrainId")
|
||||
{
|
||||
let options = {...this.state.options};
|
||||
options["baseTerrainId"] = value;
|
||||
this.setState({ options });
|
||||
}
|
||||
else
|
||||
{
|
||||
this.setState({ [name]: value });
|
||||
}
|
||||
}
|
||||
|
||||
handleError = msg => error => {
|
||||
this.props.onError(msg);
|
||||
};
|
||||
|
||||
onSubmit = values => {
|
||||
const { asset, onSubmit } = this.props;
|
||||
values = {...this.state};
|
||||
const { options = {} } = values;
|
||||
|
||||
switch (UploadDialog.AssetSourceType[asset]) {
|
||||
case SourceType.RASTER_TERRAIN:
|
||||
if (options.baseTerrainId === "")
|
||||
delete options["baseTerrainId"];
|
||||
else options.baseTerrainId = parseInt(options.baseTerrainId);
|
||||
options.toMeters = 1;
|
||||
options.heightReference = "WGS84";
|
||||
options.waterMask = false;
|
||||
break;
|
||||
case SourceType.CAPTURE:
|
||||
options.textureFormat = options.textureFormat ? "KTX2" : "AUTO";
|
||||
break;
|
||||
}
|
||||
|
||||
onSubmit(values);
|
||||
};
|
||||
|
||||
getSourceFields() {
|
||||
switch (UploadDialog.AssetSourceType[this.props.asset]) {
|
||||
case SourceType.RASTER_TERRAIN:
|
||||
let loadOptions = ({ isLoading, isError, data }) => {
|
||||
if (isLoading || isError){
|
||||
return <option disabled>LOADING...</option>;
|
||||
}
|
||||
|
||||
let userItems = data.items
|
||||
.filter(item => item.type === "TERRAIN")
|
||||
.map(item => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
));
|
||||
|
||||
return [
|
||||
<option key={"mean-sea-level"} value={""}>
|
||||
Mean Sea Level
|
||||
</option>,
|
||||
...userItems
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<IonField
|
||||
name={"options.baseTerrainId"}
|
||||
label={"Base Terrain: "}
|
||||
type={"select"}
|
||||
value={this.state.options.baseTerrainId}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
<IonFetcher
|
||||
path="assets"
|
||||
onError={this.handleError('Failed to load terrain options. Please check your token!')}
|
||||
>
|
||||
{loadOptions}
|
||||
</IonFetcher>
|
||||
</IonField>
|
||||
);
|
||||
case SourceType.CAPTURE:
|
||||
return (
|
||||
<IonField
|
||||
name={"options.textureFormat"}
|
||||
label={"Use KTX2 Compression"}
|
||||
type={"select"}
|
||||
value={this.state.options.textureFormat ? "Yes" : "No"}
|
||||
help={'KTX v2.0 is an image container format that supports Basis Universal supercompression. Use KTX2 compression to create a smaller tileset with better streaming performance.'}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
<option>No</option>
|
||||
<option>Yes</option>
|
||||
</IonField>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FormDialog
|
||||
title={this.state.title}
|
||||
show={this.props.show}
|
||||
onHide={this.props.onHide}
|
||||
handleSaveFunction={this.onSubmit}
|
||||
saveLabel={this.state.loading ? "Submitting..." : "Submit"}
|
||||
savingLabel="Submitting..."
|
||||
saveIcon={this.state.loading ? "fa fa-sync fa-spin" : "fa fa-upload"}
|
||||
ref={(domNode) => { this.dialog = domNode; }}
|
||||
>
|
||||
<IonField
|
||||
name={"name"}
|
||||
label={"Name: "}
|
||||
type={"text"}
|
||||
value={this.state.name}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<IonField
|
||||
name={"description"}
|
||||
label={"Description: "}
|
||||
type={"textarea"}
|
||||
rows={"3"}
|
||||
value={this.state.description}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<IonField
|
||||
name={"attribution"}
|
||||
label={"Attribution: "}
|
||||
type={"text"}
|
||||
value={this.state.attribution}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
{this.getSourceFields()}
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.modal-backdrop {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
.ion-upload.modal button i {
|
||||
margin-right: 1em;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import AssetType from "./AssetType";
|
||||
|
||||
const AssetConfig = {
|
||||
[AssetType.ORTHOPHOTO]: {
|
||||
name: "Orthophoto",
|
||||
icon: "far fa-image"
|
||||
},
|
||||
[AssetType.TERRAIN_MODEL]: {
|
||||
name: "Terrain Model",
|
||||
icon: "fa fa-chart-area"
|
||||
},
|
||||
[AssetType.SURFACE_MODEL]: {
|
||||
name: "Surface Model",
|
||||
icon: "fa fa-chart-area"
|
||||
},
|
||||
[AssetType.POINTCLOUD]: {
|
||||
name: "Pointcloud",
|
||||
icon: "fa fa-cube"
|
||||
},
|
||||
[AssetType.TEXTURED_MODEL]: {
|
||||
name: "Texture Model",
|
||||
icon: "fab fa-connectdevelop"
|
||||
}
|
||||
};
|
||||
|
||||
export default AssetConfig;
|
|
@ -0,0 +1,9 @@
|
|||
const AssetType = {
|
||||
ORTHOPHOTO: "ORTHOPHOTO",
|
||||
TERRAIN_MODEL: "TERRAIN_MODEL",
|
||||
SURFACE_MODEL: "SURFACE_MODEL",
|
||||
POINTCLOUD: "POINTCLOUD",
|
||||
TEXTURED_MODEL: "TEXTURED_MODEL"
|
||||
};
|
||||
|
||||
export default AssetType;
|
|
@ -0,0 +1,12 @@
|
|||
const SourceType = {
|
||||
RASTER_IMAGERY: "RASTER_IMAGERY",
|
||||
RASTER_TERRAIN: "RASTER_TERRAIN",
|
||||
TERRAIN_DATABASE: "TERRAIN_DATABASE",
|
||||
CITYGML: "CITYGML",
|
||||
KML: "KML",
|
||||
CAPTURE: "3D_CAPTURE",
|
||||
MODEL: "3D_MODEL",
|
||||
POINTCLOUD: "POINT_CLOUD"
|
||||
};
|
||||
|
||||
export default SourceType;
|
|
@ -0,0 +1,5 @@
|
|||
import AssetType from "./AssetType";
|
||||
import SourceType from "./SourceType";
|
||||
import AssetConfig from "./AssetConfig";
|
||||
|
||||
export { AssetType, SourceType, AssetConfig };
|
|
@ -0,0 +1,25 @@
|
|||
@font-face {
|
||||
font-family: 'fa-cesium';
|
||||
src:
|
||||
url('fonts/fa-cesium.ttf?rw87j5') format('truetype'),
|
||||
url('fonts/fa-cesium.woff?rw87j5') format('woff'),
|
||||
url('fonts/fa-cesium.svg?rw87j5#fa-cesium') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.fa-cesium:before {
|
||||
content: "\e900";
|
||||
|
||||
font-family: 'fa-cesium' !important;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Generated by IcoMoon</metadata>
|
||||
<defs>
|
||||
<font id="fa-cesium" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" glyph-name="cesium" horiz-adv-x="1005" d="M964.309 524.102c-16.578 0-32.613-9.381-45.192-26.269l-158.092-213.11c-26.157-35.268-63.363-55.524-102.025-55.524h-0.555c-38.686 0-75.855 20.257-102 55.524l-158.080 213.11c-12.579 16.886-28.589 26.269-45.229 26.269-16.566 0-32.626-9.381-45.155-26.269l-158.154-213.11c-25.984-35.008-62.844-55.191-101.111-55.524 80.596-173.115 253.265-293.199 453.846-293.199 277.424 0 502.419 229.232 502.419 511.962 0 20.072-1.358 39.687-3.617 58.993-11.023 11.036-23.836 17.146-37.058 17.146zM502.562 959.999c-277.535 0-502.543-229.232-502.543-511.999 0-45.019 6.258-88.409 16.961-130.046 9.48-7.394 20.084-11.764 30.848-11.764 16.677 0 32.687 9.295 45.291 26.195l158.092 213.122c26.095 35.354 63.338 55.586 101.925 55.586 38.612 0 75.793-20.232 101.975-55.586l152.055-204.925 6.58-8.197c12.554-16.799 28.528-26.058 44.995-26.195 16.418 0.136 32.404 9.394 44.933 26.195l6.641 8.197 152.105 204.925c26.095 35.354 63.313 55.586 101.901 55.586 6.11 0 12.283-0.679 18.319-1.691-63.412 208.876-254.315 360.599-480.078 360.599zM670.356 634.891c-29.021 0-52.463 23.972-52.463 53.562 0 29.515 23.442 53.438 52.463 53.438 29.058 0 52.524-23.923 52.524-53.438 0-29.601-23.466-53.562-52.524-53.562z" />
|
||||
</font></defs></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 1.6 KiB |
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"scripts": {
|
||||
"dev": "webpack --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
const makeCancelable = promise => {
|
||||
let hasCanceled_ = false;
|
||||
|
||||
const wrappedPromise = new Promise((resolve, reject) => {
|
||||
promise.then(
|
||||
val => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)),
|
||||
error =>
|
||||
hasCanceled_ ? reject({ isCanceled: true }) : reject(error)
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
promise: wrappedPromise,
|
||||
cancel() {
|
||||
hasCanceled_ = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export { makeCancelable };
|
||||
|
||||
const fetchCancelable = (...args) => makeCancelable(fetch(...args));
|
||||
|
||||
export default fetchCancelable;
|
|
@ -0,0 +1,9 @@
|
|||
export default function getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length == 2)
|
||||
return parts
|
||||
.pop()
|
||||
.split(";")
|
||||
.shift();
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import fetchCancelable from "./fetchCancelable";
|
||||
import getCookie from "./getCookie";
|
||||
|
||||
export { getCookie, fetchCancelable };
|
|
@ -0,0 +1,64 @@
|
|||
{% extends "app/plugins/templates/base.html" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.alert {
|
||||
position: absolute;
|
||||
z-index: 100000;
|
||||
width: 79vw;
|
||||
bottom: 0.5em;
|
||||
right: 1em;
|
||||
width: 18em;
|
||||
}
|
||||
#navbar-top.cesium-navbar {
|
||||
margin: -15px;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
}
|
||||
#navbar-top.cesium-navbar > .navbar-text {
|
||||
margin: 0;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
#navbar-top.cesium-navbar .description {
|
||||
font-size: 10px;
|
||||
margin-left: 28px
|
||||
}
|
||||
</style>
|
||||
<nav id="navbar-top" class="navbar-default cesium-navbar">
|
||||
<h4 class="navbar-text">
|
||||
<i class="fa fa-cesium fa-fw"></i> <strong>Cesium Ion</strong>
|
||||
<p class="description">
|
||||
Use Cesium Ion's simple workflow to create 3D maps of your geospatial
|
||||
data for visualization, analysis, and sharing
|
||||
</p>
|
||||
</h4>
|
||||
</div>
|
||||
{% if not form.token.value %}
|
||||
<h5>
|
||||
<strong>Instructions</strong>
|
||||
</h5>
|
||||
<ol>
|
||||
<li>
|
||||
Generate a token at
|
||||
<a href="https://cesium.com/ion/tokens" target="_blank"> cesium.com/ion/tokens </a>
|
||||
with <b>all permissions:</b>
|
||||
<i>assets:list, assets:read, assets:write, geocode.</i>
|
||||
</li>
|
||||
<li>Copy and paste the token into the form below.</li>
|
||||
</ol>
|
||||
{% else %}
|
||||
<p><b>You are all set!</b> To share a task, select it from the <a href="/dashboard/">dashboard</a> and press the <b>Tile in Cesium ion</b> button.</p>
|
||||
<p>
|
||||
<a class="btn btn-sm btn-primary" href="/dashboard"><i class="fa fa-external-link"></i> Go To Dashboard</a>
|
||||
<a class="btn btn-sm btn-default" href="https://cesium.com/ion" target="_blank"><i class="fa fa-cesium"></i> Open Cesium Ion</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="oam-form oam-token-form">
|
||||
<h5><b>Token Settings</b></h5>
|
||||
{% csrf_token %}
|
||||
{% include "app/plugins/templates/form.html" %}
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-save fa-fw"></i> Set Token</i></button>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
PluginsAPI.Dashboard.addTaskActionButton(
|
||||
["{{ app_name }}/build/TaskView.js"],
|
||||
function(args, TaskView) {
|
||||
if ("{{ token }}"){
|
||||
return React.createElement(TaskView, {
|
||||
task: args.task,
|
||||
token: "{{ token }}",
|
||||
apiURL: "{{ api_url }}",
|
||||
ionURL: "{{ ion_url }}"
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
|
@ -0,0 +1,216 @@
|
|||
# Arg order is very important for task deconstruction.
|
||||
# If order is changed make sure that the refresh API call is updated
|
||||
|
||||
def upload_to_ion(
|
||||
task_id,
|
||||
asset_type,
|
||||
token,
|
||||
asset_path,
|
||||
name,
|
||||
description="",
|
||||
attribution="",
|
||||
options={},
|
||||
):
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
from os import path, remove
|
||||
from shutil import rmtree
|
||||
from enum import Enum
|
||||
from app.plugins import logger
|
||||
|
||||
try:
|
||||
# Import from coreplugins if using Docker
|
||||
from coreplugins.cesiumion.api_views import (
|
||||
get_asset_info,
|
||||
set_asset_info,
|
||||
AssetType,
|
||||
ASSET_TO_OUTPUT,
|
||||
ASSET_TO_SOURCE,
|
||||
ASSET_TO_FILE,
|
||||
pluck,
|
||||
)
|
||||
from coreplugins.cesiumion.model_tools import (
|
||||
to_ion_texture_model,
|
||||
IonInvalidZip,
|
||||
)
|
||||
from coreplugins.cesiumion.globals import ION_API_URL
|
||||
except ImportError:
|
||||
# Import from plugins if imported as a plugin on exe application
|
||||
from plugins.cesiumion.api_views import (
|
||||
get_asset_info,
|
||||
set_asset_info,
|
||||
AssetType,
|
||||
ASSET_TO_OUTPUT,
|
||||
ASSET_TO_SOURCE,
|
||||
ASSET_TO_FILE,
|
||||
pluck,
|
||||
)
|
||||
from plugins.cesiumion.model_tools import (
|
||||
to_ion_texture_model,
|
||||
IonInvalidZip,
|
||||
)
|
||||
from plugins.cesiumion.globals import ION_API_URL
|
||||
|
||||
class LoggerAdapter(logging.LoggerAdapter):
|
||||
def __init__(self, prefix, logger):
|
||||
super().__init__(logger, {})
|
||||
self.prefix = prefix
|
||||
|
||||
def process(self, msg, kwargs):
|
||||
return "[%s] %s" % (self.prefix, msg), kwargs
|
||||
|
||||
class TaskUploadProgress(object):
|
||||
def __init__(self, file_path, task_id, asset_type, logger=None, log_step_size=0.05):
|
||||
self._task_id = task_id
|
||||
self._asset_type = asset_type
|
||||
self._logger = logger
|
||||
|
||||
self._uploaded_bytes = 0
|
||||
self._total_bytes = float(path.getsize(file_path))
|
||||
self._asset_info = get_asset_info(task_id, asset_type)
|
||||
|
||||
self._last_log = 0
|
||||
self._log_step_size = log_step_size
|
||||
|
||||
@property
|
||||
def asset_info(self):
|
||||
return self._asset_info
|
||||
|
||||
def __call__(self, total_bytes):
|
||||
self._uploaded_bytes += total_bytes
|
||||
progress = self._uploaded_bytes / self._total_bytes
|
||||
if progress == 1:
|
||||
progress = 1
|
||||
|
||||
self._asset_info["upload"]["progress"] = progress
|
||||
if self._logger is not None and progress - self._last_log > self._log_step_size:
|
||||
self._logger.info(f"Upload progress: {progress * 100}%")
|
||||
self._last_log = progress
|
||||
|
||||
set_asset_info(self._task_id, self._asset_type, self._asset_info)
|
||||
|
||||
asset_logger = LoggerAdapter(prefix=f"Task {task_id} {asset_type}", logger=logger)
|
||||
asset_type = AssetType[asset_type]
|
||||
asset_info = get_asset_info(task_id, asset_type)
|
||||
del_directory = None
|
||||
|
||||
try:
|
||||
import boto3
|
||||
except ImportError:
|
||||
import subprocess
|
||||
|
||||
asset_logger.info(f"Manually installing boto3...")
|
||||
subprocess.call([sys.executable, "-m", "pip", "install", "boto3"])
|
||||
import boto3
|
||||
|
||||
try:
|
||||
# Update asset_path based off
|
||||
if asset_type == AssetType.TEXTURED_MODEL:
|
||||
try:
|
||||
generated_zipfile = asset_path
|
||||
asset_path, del_directory = to_ion_texture_model(asset_path)
|
||||
logger.info("Created ion texture model!")
|
||||
except IonInvalidZip as e:
|
||||
logger.info("Non geo-referenced texture model, using default file.")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to convert to ion texture model: {e}")
|
||||
|
||||
if path.isfile(generated_zipfile):
|
||||
remove(generated_zipfile)
|
||||
logger.info(f"File {generated_zipfile} has been deleted.")
|
||||
else:
|
||||
logger.warning(f"The path {generated_zipfile} does not exist.")
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
data = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"attribution": attribution,
|
||||
"type": ASSET_TO_OUTPUT[asset_type],
|
||||
"options": {**options, "sourceType": ASSET_TO_SOURCE[asset_type]},
|
||||
}
|
||||
|
||||
# Create Asset Request
|
||||
asset_logger.info(f"Creating asset of type {asset_type}")
|
||||
res = requests.post(f"{ION_API_URL}/assets", json=data, headers=headers)
|
||||
res.raise_for_status()
|
||||
ion_info, upload_meta, on_complete = pluck(
|
||||
res.json(), "assetMetadata", "uploadLocation", "onComplete"
|
||||
)
|
||||
ion_id = ion_info["id"]
|
||||
access_key, secret_key, token, endpoint, bucket, file_prefix = pluck(
|
||||
upload_meta,
|
||||
"accessKey",
|
||||
"secretAccessKey",
|
||||
"sessionToken",
|
||||
"endpoint",
|
||||
"bucket",
|
||||
"prefix",
|
||||
)
|
||||
|
||||
# Upload
|
||||
asset_logger.info("Starting upload")
|
||||
uploat_stats = TaskUploadProgress(asset_path, task_id, asset_type, asset_logger)
|
||||
key = path.join(file_prefix, ASSET_TO_FILE[asset_type])
|
||||
boto3.client(
|
||||
"s3",
|
||||
endpoint_url=endpoint,
|
||||
aws_access_key_id=access_key,
|
||||
aws_secret_access_key=secret_key,
|
||||
aws_session_token=token,
|
||||
).upload_file(asset_path, Bucket=bucket, Key=key, Callback=uploat_stats)
|
||||
asset_info = uploat_stats.asset_info
|
||||
asset_info["id"] = ion_id
|
||||
asset_info["upload"]["active"] = False
|
||||
asset_info["process"]["active"] = True
|
||||
set_asset_info(task_id, asset_type, asset_info)
|
||||
|
||||
# On Complete Handler
|
||||
asset_logger.info("Upload complete")
|
||||
method, url, fields = pluck(on_complete, "method", "url", "fields")
|
||||
res = requests.request(method, url=url, headers=headers, data=fields)
|
||||
res.raise_for_status()
|
||||
|
||||
# Processing Status Refresh
|
||||
asset_logger.info("Starting processing")
|
||||
refresh = True
|
||||
while refresh:
|
||||
res = requests.get(f"{ION_API_URL}/assets/{ion_id}", headers=headers)
|
||||
res.raise_for_status()
|
||||
|
||||
state, percent_complete = pluck(res.json(), "status", "percentComplete")
|
||||
progress = float(percent_complete) / 100
|
||||
if "ERROR" in state.upper():
|
||||
asset_info["error"] = f"Processing failed"
|
||||
asset_logger.info("Processing failed...")
|
||||
refresh = False
|
||||
if progress >= 1:
|
||||
refresh = False
|
||||
|
||||
if asset_info["process"]["progress"] != progress:
|
||||
asset_info["process"]["progress"] = progress
|
||||
asset_logger.info(f"Processing {percent_complete}% - {state}")
|
||||
set_asset_info(task_id, asset_type, asset_info)
|
||||
time.sleep(2)
|
||||
|
||||
asset_logger.info("Processing complete")
|
||||
asset_info["process"]["progress"] = 1
|
||||
asset_info["process"]["active"] = False
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 401:
|
||||
asset_info["error"] = "Invalid ion token!"
|
||||
elif e.response.status_code == 404:
|
||||
asset_info["error"] = "Missing permisssions on ion token!"
|
||||
else:
|
||||
asset_info["error"] = str(e)
|
||||
asset_logger.error(e)
|
||||
except Exception as e:
|
||||
asset_info["error"] = str(e)
|
||||
asset_logger.error(e)
|
||||
|
||||
if del_directory != None:
|
||||
rmtree(del_directory)
|
||||
|
||||
set_asset_info(task_id, asset_type, asset_info)
|
Ładowanie…
Reference in New Issue