Archive cesium ion plugin

pull/1109/head
Piero Toffanin 2021-12-01 09:33:43 -05:00
rodzic 754b3a5896
commit 3f939eeae8
37 zmienionych plików z 0 dodań i 1929 usunięć

Wyświetl plik

@ -1,25 +0,0 @@
<p align="center">
<img src="https://github.com/AnalyticalGraphicsInc/cesium/wiki/logos/Cesium_Logo_Color.jpg" width="50%" />
</p>
The Cesium Ion WebODM add-on enables you to publish and stream even the most massive of 3D Content on the web thanks to Cesium Ion.
With 3D Tiles, even multi-gigabyte models can be streamed to any device without having to download the entire model up front. By loading 3D Tiles into CesiumJS, you can fuse your model with other datasets, add geospatial context to place it at a real world location, or overlay additional details and analysis.
Learn more at https://cesium.com
## Setup
1. Grab a token with, `assets:list, assets:read, assets:write` permissions from [Cesium Ion](https://cesium.com/ion/tokens).
1. Under the Cesium Ion tab in WebODM set the token to the code generated in ion.
## Testing
1. WebODM provides open datasets which can be used as a testbench for the add-on. You will have to create an account in order to access the data. Download datasets [here](https://demo.webodm.org/dashboard/).
1. To use the dataset click on a **task**, then in the **download assets** dropdown, select all. This should download a zip on to your machine.
1. In your instance of WebODM, create a project, click import in the upper-right hand corner, and selet the zip we just downloaded from the demo.
1. After the import completes and a website reload all Cesium Ion functions should be available for testing.

Wyświetl plik

@ -1,22 +0,0 @@
[
{
"name": "formik",
"url": "https://github.com/jaredpalmer/formik",
"license": "MIT"
},
{
"name": "yup",
"url": "https://github.com/jquense/yup",
"license": "MIT"
},
{
"name": "react-bootstrap",
"url": "https://github.com/react-bootstrap/react-bootstrap",
"license": "MIT"
},
{
"name": "boto3",
"url": "https://github.com/boto/boto3",
"license": "Apache-2.0"
}
]

Wyświetl plik

@ -1 +0,0 @@
from .plugin import *

Wyświetl plik

@ -1,314 +0,0 @@
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):
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:
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)

Wyświetl plik

@ -1,71 +0,0 @@
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

Wyświetl plik

@ -1,2 +0,0 @@
PROJECT_NAME = __name__.split(".")[-2]
ION_API_URL = "https://api.cesium.com/v1"

Wyświetl plik

@ -1,13 +0,0 @@
{
"name": "Cesium Ion",
"webodmMinVersion": "0.6.0",
"description": "Upload and tile ODM assets with Cesium ion.",
"version": "1.2.1",
"author": "Cesium GS, Inc",
"email": "hello@cesium.com",
"repository": "",
"tags": ["cesium ion", "ceium"],
"homepage": "https://cesium.com",
"experimental": true,
"deprecated": false
}

Wyświetl plik

@ -1,71 +0,0 @@
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

Wyświetl plik

@ -1,39 +0,0 @@
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)),
]

Wyświetl plik

@ -1,268 +0,0 @@
import React, { Component, Fragment } from "react";
import ErrorMessage from "webodm/components/ErrorMessage";
import { Button } from "react-bootstrap";
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 { AssetStyles } from "./defaults";
import { fetchCancelable, getCookie } from "./utils";
export default class TaskView extends Component {
state = {
error: "",
currentAsset: null,
isTasksDialog: false,
isUploadDialogLoading: false
};
cancelableFetch = null;
timeoutHandler = null;
refreshAssets = null;
onOpenUploadDialog = asset => this.setState({ currentAsset: asset });
onHideUploadDialog = () =>
this.setState({ currentAsset: null, isUploadDialogLoading: false });
showTaskDialog = () => this.setState({ isTasksDialog: true });
hideTaskDialog = () => this.setState({ isTasksDialog: false });
onUploadAsset = data => {
const { task, token, apiURL } = this.props;
const { currentAsset } = this.state;
const payload = Object.assign({}, data);
if (currentAsset === null) {
console.warning("Submissions on invalid asset");
return;
}
payload.token = token;
payload.asset_type = currentAsset;
this.setState({ isUploadDialogLoading: true });
this.cancelableFetch = 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;
}, {});
window.open(`https://cesium.com/ion/assets/${idMap[asset]}`);
};
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.warning("Failed to clear dead requests");
console.warning(exception);
}
}
this.refreshAssets = null;
}
render() {
const { task, token } = this.props;
const {
isTasksDialog,
isUploadDialogLoading,
isRefreshTask,
currentAsset
} = this.state;
const isUploadDialog = currentAsset !== null;
const assetName = isUploadDialog ? AssetStyles[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 =>
(!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 isTasks = processing.length > 0;
const isErrors = 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"}
bsStyle={"primary"}
bsSize={"small"}
onClick={this.refreshAssets}
>
<i className={"fa fa-cesium"} />
Refresh Available ion Assets
</Button>
)}
{isTasks && (
<Button
className={"ion-btn"}
bsStyle={
isErrors ? "danger" : "primary"
}
bsSize={"small"}
onClick={this.showTaskDialog}
>
<i className={"fa fa-cesium"} />
View ion Tasks
</Button>
)}
<TasksDialog
show={isTasksDialog}
tasks={processing}
onHide={this.hideTaskDialog}
onClearFailed={this.onClearFailedAssets}
/>
</Fragment>
);
}}
</TaskFetcher>
</div>
<APIFetcher path={"projects/"} params={{ id: task.project }}>
{({ isLoading, isError, data }) => {
const initialValues = {};
if (!isLoading && !isError && data.results.length > 0) {
const project = data.results[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}
/>
);
}}
</APIFetcher>
<TaskFetcher
method={"POST"}
path={"refresh"}
body={JSON.stringify({ token })}
onLoad={this.onCleanStatus}
/>
</AppContext.Provider>
);
}
}

Wyświetl plik

@ -1,8 +0,0 @@
const AppContext = React.createContext({
apiUrl: null,
ionURL: null,
token: null,
task: null
});
export default AppContext;

Wyświetl plik

@ -1,50 +0,0 @@
import { Formik, Field, getIn } from "formik";
import {
FormGroup,
ControlLabel,
FormControl,
Checkbox,
HelpBlock
} from "react-bootstrap";
const BootstrapFieldComponent = ({
field,
form: { touched, errors },
label,
help,
type = "",
showIcon = true,
...props
}) => {
const isError = getIn(errors, field.name) && getIn(touched, field.name);
const errorMsg = getIn(errors, field.name);
let ControlComponent = FormControl;
const testType = type.toLowerCase();
if (testType === "checkbox") ControlComponent = Checkbox;
else if (testType === "textarea" || testType === "select")
props.componentClass = testType;
else props.type = type;
return (
<FormGroup
controlId={field.name}
validationState={isError ? "error" : null}
style={{ marginLeft: 0, marginRight: 0 }}
>
{label && <ControlLabel>{label}</ControlLabel>}
<ControlComponent {...field} {...props} />
{isError && <HelpBlock>{errorMsg}</HelpBlock>}
{help && !isError && <HelpBlock>{help}</HelpBlock>}
{isError && showIcon && <FormControl.Feedback />}
</FormGroup>
);
};
const BootstrapField = props => (
<Field component={BootstrapFieldComponent} {...props} />
);
export { BootstrapFieldComponent };
export default BootstrapField;

Wyświetl plik

@ -1,140 +0,0 @@
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 };

Wyświetl plik

@ -1,35 +0,0 @@
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);

Wyświetl plik

@ -1,60 +0,0 @@
import React, { PureComponent, Fragment } from "react";
import { DropdownButton, MenuItem } from "react-bootstrap";
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 => (
<MenuItem
key={asset}
tag={"a"}
onClick={this.handleClick(asset)}
>
<AssetComponent asset={asset} showIcon={true} />
</MenuItem>
));
const title = (
<Fragment>
<i className={"fa fa-cesium"} />
{children}
</Fragment>
);
return (
<DropdownButton
id={"cesiumIonUploadDropdown"}
bsStyle={"primary"}
bsSize={"small"}
className={"ion-btn"}
title={title}
>
{menuItems}
</DropdownButton>
);
}
}

Wyświetl plik

@ -1,14 +0,0 @@
.ion-btn {
.fa-cesium {
margin-right: 0.5em;
}
.caret {
margin-left: 1em;
}
}
.ion-dropdowns .dropdown {
float: none;
margin-right: 4px;
}

Wyświetl plik

@ -1,12 +0,0 @@
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;

Wyświetl plik

@ -1,19 +0,0 @@
.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;
}
}

Wyświetl plik

@ -1,130 +0,0 @@
import React, { Component, Fragment } from "react";
import {
Row,
Col,
Modal,
Button,
ListGroup,
ListGroupItem,
ProgressBar,
Glyphicon
} from "react-bootstrap";
import IonAssetLabel from "./IonAssetLabel";
import "./TaskDialog.scss";
const TaskStatusItem = ({
asset,
progress,
task,
helpText = "",
active = true,
bsStyle = "primary"
}) => (
<ListGroupItem>
<Row>
<Col xs={6}>
<p style={{ fontWeight: "bold" }}>
<IonAssetLabel asset={asset} showIcon={true} />
</p>
</Col>
<Col xs={6}>
<p className={"pull-right"}>Status: {task}</p>
</Col>
</Row>
<ProgressBar active={active} now={progress} bsStyle={bsStyle} />
{helpText && <small>{helpText}</small>}
</ListGroupItem>
);
export default class TaskDialog extends Component {
static defaultProps = {
tasks: [],
taskComponent: TaskStatusItem
};
render() {
const {
tasks,
taskComponent: TaskComponent,
onClearFailed,
onHide,
...options
} = this.props;
let hasErrors = false;
const taskItems = tasks.map(
({ type: asset, upload, process, error }) => {
let task,
style,
active = true,
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";
active = false;
console.error(error);
hasErrors = true;
}
return (
<TaskStatusItem
key={asset}
asset={asset}
progress={progress * 100}
task={task}
bsStyle={style}
helpText={error}
/>
);
}
);
return (
<Modal className={"ion-tasks"} onHide={onHide} {...options}>
<Modal.Header closeButton>
<Modal.Title>
<i className={"fa fa-cesium"} /> Cesium Ion Tasks
</Modal.Title>
</Modal.Header>
<Modal.Body>
<ListGroup>{taskItems}</ListGroup>
{hasErrors && (
<Button
className={"center-block"}
bsSize={"small"}
bsStyle={"danger"}
onClick={onClearFailed}
>
<Glyphicon
style={{ marginRight: "0.5em" }}
glyph={"trash"}
/>
Remove Failed Tasks
</Button>
)}
</Modal.Body>
<Modal.Footer>
<Button bsStyle={"primary"} onClick={onHide}>
Close
</Button>
</Modal.Footer>
</Modal>
);
}
}

Wyświetl plik

@ -1,225 +0,0 @@
import React, { Component, Fragment } from "react";
import FormDialog from "webodm/components/FormDialog";
import { Formik } from "formik";
import * as Yup from "yup";
import { Row, Col, Modal, Button } from "react-bootstrap";
import BootstrapField from "./BootstrapField";
import FormikErrorFocus from "./FormikErrorFocus";
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
}
}
};
handleError = msg => error => {
this.props.onError(msg);
console.error(error);
};
onSubmit = values => {
const { asset, onSubmit } = this.props;
values = JSON.parse(JSON.stringify(values));
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 ? "WEBP" : "AUTO";
break;
}
onSubmit(values);
};
getSourceFields() {
switch (UploadDialog.AssetSourceType[this.props.asset]) {
case SourceType.RASTER_TERRAIN:
const loadOptions = ({ isLoading, isError, data }) => {
if (isLoading || isError)
return <option disabled>LOADING...</option>;
const 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 (
<BootstrapField
name={"options.baseTerrainId"}
label={"Base Terrain: "}
type={"select"}
>
<IonFetcher
path="assets"
onError={this.handleError(
"Failed to load terrain options. " +
"Please check your token!"
)}
>
{loadOptions}
</IonFetcher>
</BootstrapField>
);
case SourceType.CAPTURE:
return (
<BootstrapField
name={"options.textureFormat"}
type={"checkbox"}
help={
"Will produce WebP images, which are typically 25-34% smaller than " +
"equivalent JPEG images which leads to faster streaming and reduced " +
"data usage. 3D Tiles produced with this option require a client " +
"that supports the glTF EXT_texture_webp extension, such as " +
"CesiumJS 1.54 or newer, and a browser that supports WebP, such as " +
"Chrome or Firefox 65 and newer."
}
>
Use WebP images
</BootstrapField>
);
default:
return null;
}
}
getValidation() {
const schema = {};
switch (UploadDialog.AssetSourceType[this.props.asset]) {
case SourceType.RASTER_TERRAIN:
schema.baseTerrainId = Yup.string();
break;
case SourceType.CAPTURE:
schema.textureFormat = Yup.boolean();
break;
}
return Yup.object().shape({
name: Yup.string().required("A name is required!"),
options: Yup.object()
.shape(schema)
.default({})
});
}
render() {
const {
initialValues,
onHide,
title,
loading: isLoading,
...options
} = this.props;
delete options["asset"];
delete options["onSubmit"];
const mergedInitialValues = {
...UploadDialog.defaultProps.initialValues,
...initialValues
};
return (
<Modal className={"ion-upload"} onHide={onHide} {...options}>
<Modal.Header closeButton>
<Modal.Title>
<i className={"fa fa-cesium"} /> {title}
</Modal.Title>
</Modal.Header>
<Formik
initialValues={mergedInitialValues}
onSubmit={this.onSubmit}
enableReinitialize
validationSchema={this.getValidation()}
>
{({ handleSubmit = () => {} }) => (
<form>
<Modal.Body>
<BootstrapField
name={"name"}
label={"Name: "}
type={"text"}
/>
<BootstrapField
name={"description"}
label={"Description: "}
type={"textarea"}
rows={"3"}
/>
<BootstrapField
name={"attribution"}
label={"Attribution: "}
type={"text"}
/>
{this.getSourceFields()}
<FormikErrorFocus />
</Modal.Body>
<Modal.Footer>
<Button onClick={onHide}>Close</Button>
{isLoading && (
<Button bsStyle="primary" disabled>
<i
className={"fa fa-sync fa-spin"}
/>
Submitting...
</Button>
)}
{!isLoading && (
<Button
bsStyle="primary"
onClick={handleSubmit}
>
<i className={"fa fa-upload"} />
Submit
</Button>
)}
</Modal.Footer>
</form>
)}
</Formik>
</Modal>
);
}
}

Wyświetl plik

@ -1,7 +0,0 @@
.modal-backdrop {
z-index: 100000 !important;
}
.ion-upload.modal button i {
margin-right: 1em;
}

Wyświetl plik

@ -1,26 +0,0 @@
import AssetType from "./AssetType";
const AssetStyles = {
[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 AssetStyles;

Wyświetl plik

@ -1,9 +0,0 @@
const AssetType = {
ORTHOPHOTO: "ORTHOPHOTO",
TERRAIN_MODEL: "TERRAIN_MODEL",
SURFACE_MODEL: "SURFACE_MODEL",
POINTCLOUD: "POINTCLOUD",
TEXTURED_MODEL: "TEXTURED_MODEL"
};
export default AssetType;

Wyświetl plik

@ -1,12 +0,0 @@
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;

Wyświetl plik

@ -1,5 +0,0 @@
import AssetType from "./AssetType";
import SourceType from "./SourceType";
import AssetStyles from "./AssetStyles";
export { AssetType, SourceType, AssetStyles };

Wyświetl plik

@ -1,25 +0,0 @@
@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;
}

Wyświetl plik

@ -1,11 +0,0 @@
<?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="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" 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>

Przed

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

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -1,11 +0,0 @@
{
"scripts": {
"dev": "webpack --watch"
},
"dependencies": {
"formik": "^1.5.7",
"moment": "^2.24.0",
"react-bootstrap": "^0.32.4",
"yup": "^0.27.0"
}
}

Wyświetl plik

@ -1,24 +0,0 @@
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;

Wyświetl plik

@ -1,9 +0,0 @@
export default function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length == 2)
return parts
.pop()
.split(";")
.shift();
}

Wyświetl plik

@ -1,4 +0,0 @@
import fetchCancelable from "./fetchCancelable";
import getCookie from "./getCookie";
export { getCookie, fetchCancelable };

Wyświetl plik

@ -1,64 +0,0 @@
{% 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 %}

Wyświetl plik

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

Wyświetl plik

@ -1,190 +0,0 @@
# 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
from shutil import rmtree
from enum import Enum
from app.plugins import logger
from .api_views import (
get_asset_info,
set_asset_info,
AssetType,
ASSET_TO_OUTPUT,
ASSET_TO_SOURCE,
ASSET_TO_FILE,
pluck,
)
from .model_tools import (
to_ion_texture_model,
IonInvalidZip,
)
from .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:
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("Failed to convert to ion texture model")
logger.warning(e)
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)