Merge pull request #1536 from Aero-Ai/usewebodmcomponents

Restore the Cesium Ion Plugin
pull/1537/head
Piero Toffanin 2024-08-09 14:22:30 -04:00 zatwierdzone przez GitHub
commit 58f4d3830f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
38 zmienionych plików z 2109 dodań i 4 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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"
}
]

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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="&#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>

Po

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

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -0,0 +1,7 @@
{
"scripts": {
"dev": "webpack --watch"
},
"dependencies": {
}
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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