kopia lustrzana https://github.com/OpenDroneMap/WebODM
Export from Download Assets (orthophoto) working
rodzic
8054b650f9
commit
34736e63b0
|
@ -150,7 +150,7 @@ class Metadata(TaskNestedView):
|
|||
raise exceptions.NotFound()
|
||||
try:
|
||||
with COGReader(raster_path) as src:
|
||||
band_count = src.metadata()['count']
|
||||
band_count = src.dataset.meta['count']
|
||||
if boundaries_feature is not None:
|
||||
boundaries_cutline = create_cutline(src.dataset, boundaries_feature, CRS.from_string('EPSG:4326'))
|
||||
boundaries_bbox = featureBounds(boundaries_feature)
|
||||
|
@ -551,7 +551,6 @@ class Export(TaskNestedView):
|
|||
rescale=rescale,
|
||||
color_map=color_map,
|
||||
hillshade=hillshade,
|
||||
dem=asset_type in ['dsm', 'dtm'],
|
||||
name=task.name,
|
||||
asset_type=asset_type).task_id
|
||||
asset_type=asset_type,
|
||||
name=task.name).task_id
|
||||
return Response({'celery_task_id': celery_task_id, 'filename': filename})
|
||||
|
|
|
@ -12,6 +12,7 @@ from rio_tiler.colormap import cmap as colormap, apply_cmap
|
|||
from rio_tiler.errors import InvalidColorMapName
|
||||
from app.api.hsvblend import hsv_blend
|
||||
from app.api.hillshade import LightSource
|
||||
from rio_tiler.io import COGReader
|
||||
from rasterio.warp import calculate_default_transform, reproject, Resampling
|
||||
|
||||
logger = logging.getLogger('app.logger')
|
||||
|
@ -35,11 +36,13 @@ def export_raster(input, output, **opts):
|
|||
rescale = opts.get('rescale')
|
||||
color_map = opts.get('color_map')
|
||||
hillshade = opts.get('hillshade')
|
||||
dem = opts.get('dem')
|
||||
asset_type = opts.get('asset_type')
|
||||
name = opts.get('name', 'raster') # KMZ specific
|
||||
asset_type = opts.get('asset_type', 'raster') # KMZ specific
|
||||
|
||||
dem = asset_type in ['dsm', 'dtm']
|
||||
|
||||
with rasterio.open(input) as src:
|
||||
with COGReader(input) as ds_src:
|
||||
src = ds_src.dataset
|
||||
profile = src.meta.copy()
|
||||
|
||||
# Output format
|
||||
|
@ -78,7 +81,12 @@ def export_raster(input, output, **opts):
|
|||
band_count = src.count
|
||||
|
||||
if rgb and rescale is None:
|
||||
rescale = [0,255]
|
||||
# Compute min max
|
||||
nodata = None
|
||||
if asset_type == 'orthophoto':
|
||||
nodata = 0
|
||||
md = ds_src.metadata(pmin=2.0, pmax=98.0, hist_options={"bins": 255}, nodata=nodata)
|
||||
rescale = [md['statistics']['1']['min'], md['statistics']['1']['max']]
|
||||
|
||||
ci = src.colorinterp
|
||||
|
||||
|
@ -117,7 +125,13 @@ def export_raster(input, output, **opts):
|
|||
arr = arr.astype(np.uint8)
|
||||
|
||||
return arr
|
||||
|
||||
|
||||
def update_rgb_colorinterp(dst):
|
||||
if with_alpha:
|
||||
dst.colorinterp = [ColorInterp.red, ColorInterp.green, ColorInterp.blue, ColorInterp.alpha]
|
||||
else:
|
||||
dst.colorinterp = [ColorInterp.red, ColorInterp.green, ColorInterp.blue]
|
||||
|
||||
profile.update(driver=driver, count=band_count)
|
||||
if rgb:
|
||||
profile.update(dtype=rasterio.uint8)
|
||||
|
@ -202,6 +216,8 @@ def export_raster(input, output, **opts):
|
|||
|
||||
if with_alpha:
|
||||
write_band(mask, dst, band_num)
|
||||
|
||||
update_rgb_colorinterp(dst)
|
||||
else:
|
||||
# Raw
|
||||
write_band(process(arr)[0], dst, 1)
|
||||
|
@ -230,9 +246,11 @@ def export_raster(input, output, **opts):
|
|||
for b in rgb_data:
|
||||
write_band(process(b, skip_rescale=True), dst, band_num)
|
||||
band_num += 1
|
||||
|
||||
|
||||
if with_alpha:
|
||||
write_band(mask, dst, band_num)
|
||||
|
||||
update_rgb_colorinterp(dst)
|
||||
else:
|
||||
# Raw
|
||||
write_band(process(arr)[0], dst, 1)
|
||||
|
@ -251,7 +269,13 @@ def export_raster(input, output, **opts):
|
|||
else:
|
||||
write_band(process(arr), dst, band_num)
|
||||
band_num += 1
|
||||
|
||||
|
||||
new_ci = [src.colorinterp[idx - 1] for idx in indexes]
|
||||
if not with_alpha:
|
||||
new_ci = [ci for ci in new_ci if ci != ColorInterp.alpha]
|
||||
|
||||
dst.colorinterp = new_ci
|
||||
|
||||
if kmz:
|
||||
subprocess.check_output(["gdal_translate", "-of", "KMLSUPEROVERLAY",
|
||||
"-co", "Name={}".format(name),
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
import { _ } from './gettext';
|
||||
|
||||
class AssetDownload{
|
||||
constructor(label, asset, icon){
|
||||
constructor(label, asset, icon, exportFormats = null){
|
||||
this.label = label;
|
||||
this.asset = asset;
|
||||
this.icon = icon;
|
||||
this.exportFormats = exportFormats;
|
||||
}
|
||||
|
||||
downloadUrl(project_id, task_id){
|
||||
return `/api/projects/${project_id}/tasks/${task_id}/download/${this.asset}`;
|
||||
}
|
||||
|
||||
exportId(){
|
||||
// Export identifier is the same as the asset value (minus the extension)
|
||||
return this.asset.replace(/\..+$/, "");
|
||||
}
|
||||
|
||||
get separator(){
|
||||
return false;
|
||||
}
|
||||
|
@ -33,14 +39,12 @@ class AssetDownloadSeparator extends AssetDownload{
|
|||
const api = {
|
||||
all: function() {
|
||||
return [
|
||||
new AssetDownload(_("Orthophoto (GeoTIFF)"),"orthophoto.tif","far fa-image"),
|
||||
new AssetDownload(_("Orthophoto (PNG)"),"orthophoto.png","far fa-image"),
|
||||
new AssetDownload(_("Orthophoto"),"orthophoto.tif","far fa-image", ["gtiff", "gtiff-rgb", "jpg", "png", "kmz"]),
|
||||
new AssetDownload(_("Orthophoto (MBTiles)"),"orthophoto.mbtiles","far fa-image"),
|
||||
new AssetDownload(_("Orthophoto (Tiles)"),"orthophoto_tiles.zip","fa fa-table"),
|
||||
new AssetDownload(_("Orthophoto (KMZ)"),"orthophoto.kmz","fa fa-globe"),
|
||||
new AssetDownload(_("Terrain Model (GeoTIFF)"),"dtm.tif","fa fa-chart-area"),
|
||||
new AssetDownload(_("Terrain Model"),"dtm.tif","fa fa-chart-area"),
|
||||
new AssetDownload(_("Terrain Model (Tiles)"),"dtm_tiles.zip","fa fa-table"),
|
||||
new AssetDownload(_("Surface Model (GeoTIFF)"),"dsm.tif","fa fa-chart-area"),
|
||||
new AssetDownload(_("Surface Model"),"dsm.tif","fa fa-chart-area"),
|
||||
new AssetDownload(_("Surface Model (Tiles)"),"dsm_tiles.zip","fa fa-table"),
|
||||
new AssetDownload(_("Point Cloud (LAS)"),"georeferenced_model.las","fa fa-cube"),
|
||||
new AssetDownload(_("Point Cloud (LAZ)"),"georeferenced_model.laz","fa fa-cube"),
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import '../css/AssetDownloadButtons.scss';
|
||||
import AssetDownloads from '../classes/AssetDownloads';
|
||||
import PropTypes from 'prop-types';
|
||||
import ExportAssetDialog from './ExportAssetDialog';
|
||||
import { _ } from '../classes/gettext';
|
||||
|
||||
class AssetDownloadButtons extends React.Component {
|
||||
|
@ -23,12 +24,30 @@ class AssetDownloadButtons extends React.Component {
|
|||
|
||||
constructor(props){
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
exportDialogProps: null
|
||||
}
|
||||
}
|
||||
|
||||
onHide = () => {
|
||||
this.setState({exportDialogProps: null});
|
||||
}
|
||||
|
||||
render(){
|
||||
const assetDownloads = AssetDownloads.only(this.props.task.available_assets);
|
||||
|
||||
return (<div className={"asset-download-buttons " + (this.props.showLabel ? "btn-group" : "") + " " + (this.props.direction === "up" ? "dropup" : "")}>
|
||||
|
||||
{this.state.exportDialogProps ?
|
||||
<ExportAssetDialog task={this.props.task}
|
||||
asset={this.state.exportDialogProps.asset}
|
||||
exportFormats={this.state.exportDialogProps.exportFormats}
|
||||
onHide={this.onHide}
|
||||
assetLabel={this.state.exportDialogProps.assetLabel}
|
||||
/>
|
||||
: ""}
|
||||
|
||||
<button type="button" className={"btn btn-sm " + this.props.buttonClass} disabled={this.props.disabled} data-toggle="dropdown">
|
||||
<i className="glyphicon glyphicon-download"></i>{this.props.showLabel ? " " + _("Download Assets") : ""}
|
||||
</button>
|
||||
|
@ -38,12 +57,23 @@ class AssetDownloadButtons extends React.Component {
|
|||
</button> : ""}
|
||||
<ul className="dropdown-menu">
|
||||
{assetDownloads.map((asset, i) => {
|
||||
if (!asset.separator){
|
||||
return (<li key={i}>
|
||||
<a href={asset.downloadUrl(this.props.task.project, this.props.task.id)}><i className={asset.icon + " fa-fw"}></i> {asset.label}</a>
|
||||
</li>);
|
||||
}else{
|
||||
if (asset.separator){
|
||||
return (<li key={i} className="divider"></li>);
|
||||
}else{
|
||||
let onClick = undefined;
|
||||
if (asset.exportFormats){
|
||||
onClick = e => {
|
||||
e.preventDefault();
|
||||
this.setState({exportDialogProps: {
|
||||
asset: asset.exportId(),
|
||||
exportFormats: asset.exportFormats,
|
||||
assetLabel: asset.label
|
||||
}});
|
||||
}
|
||||
}
|
||||
return (<li key={i}>
|
||||
<a href={asset.downloadUrl(this.props.task.project, this.props.task.id)} onClick={onClick}><i className={asset.icon + " fa-fw"}></i> {asset.label}</a>
|
||||
</li>);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import '../css/ExportAssetDialog.scss';
|
||||
import React from 'react';
|
||||
import FormDialog from './FormDialog';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _ } from '../classes/gettext';
|
||||
import ExportAssetPanel from './ExportAssetPanel';
|
||||
|
||||
class ExportAssetDialog extends React.Component {
|
||||
static defaultProps = {
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
onHide: PropTypes.func.isRequired,
|
||||
asset: PropTypes.string.isRequired,
|
||||
task: PropTypes.object.isRequired,
|
||||
exportFormats: PropTypes.arrayOf(PropTypes.string),
|
||||
assetLabel: PropTypes.string
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleSave = (cb) => {
|
||||
this.exportAssetPanel.handleExport()(cb);
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div className="export-asset-dialog">
|
||||
<FormDialog
|
||||
getFormData={() => {}}
|
||||
reset={() => {}}
|
||||
show={true}
|
||||
saveIcon="glyphicon glyphicon-download"
|
||||
title={this.props.assetLabel}
|
||||
savingLabel={_("Downloading…")}
|
||||
saveLabel={_("Download")}
|
||||
saveAction={() => {}}
|
||||
handleSaveFunction={this.handleSave}
|
||||
onHide={this.props.onHide}>
|
||||
<ExportAssetPanel asset={this.props.asset}
|
||||
task={this.props.task}
|
||||
ref={(domNode) => { this.exportAssetPanel = domNode; }}
|
||||
selectorOnly
|
||||
exportFormats={this.props.exportFormats} />
|
||||
</FormDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ExportAssetDialog;
|
|
@ -13,7 +13,8 @@ export default class ExportAssetPanel extends React.Component {
|
|||
asset: "",
|
||||
exportParams: {},
|
||||
task: null,
|
||||
dropUp: false
|
||||
dropUp: false,
|
||||
selectorOnly: false
|
||||
};
|
||||
static propTypes = {
|
||||
exportFormats: PropTypes.arrayOf(PropTypes.string),
|
||||
|
@ -23,7 +24,8 @@ export default class ExportAssetPanel extends React.Component {
|
|||
PropTypes.object
|
||||
]),
|
||||
task: PropTypes.object.isRequired,
|
||||
dropUp: PropTypes.bool
|
||||
dropUp: PropTypes.bool,
|
||||
selectorOnly: PropTypes.bool
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
|
@ -71,7 +73,6 @@ export default class ExportAssetPanel extends React.Component {
|
|||
this.setState({format: e.target.value});
|
||||
}
|
||||
|
||||
|
||||
handleSelectEpsg = e => {
|
||||
this.setState({epsg: e.target.value});
|
||||
}
|
||||
|
@ -95,7 +96,9 @@ export default class ExportAssetPanel extends React.Component {
|
|||
}
|
||||
|
||||
handleExport = (format) => {
|
||||
return () => {
|
||||
if (!format) format = this.state.format;
|
||||
|
||||
return (cb) => {
|
||||
const { task } = this.props;
|
||||
this.setState({exporting: true, error: ""});
|
||||
const data = this.getExportParams(format);
|
||||
|
@ -109,23 +112,35 @@ export default class ExportAssetPanel extends React.Component {
|
|||
}).done(result => {
|
||||
if (result.celery_task_id){
|
||||
Workers.waitForCompletion(result.celery_task_id, error => {
|
||||
if (error) this.setState({exporting: false, error});
|
||||
else{
|
||||
if (error){
|
||||
this.setState({exporting: false, error});
|
||||
if (cb !== undefined) cb(new Error(error));
|
||||
}else{
|
||||
this.setState({exporting: false});
|
||||
Workers.downloadFile(result.celery_task_id, result.filename);
|
||||
if (cb !== undefined) cb();
|
||||
}
|
||||
});
|
||||
}else if (result.url){
|
||||
// Simple download
|
||||
this.setState({exporting: false});
|
||||
window.location.href = `${result.url}?filename=${result.filename}`;
|
||||
if (cb !== undefined) cb();
|
||||
}else if (result.error){
|
||||
this.setState({exporting: false, error: result.error});
|
||||
if (cb !== undefined) cb(new Error(result.error));
|
||||
}else{
|
||||
this.setState({exporting: false, error: interpolate(_("Invalid JSON response: %(error)s"), {error: JSON.stringify(result)})});
|
||||
let error = interpolate(_("Invalid JSON response: %(error)s"), {error: JSON.stringify(result)});
|
||||
|
||||
this.setState({exporting: false, error});
|
||||
if (cb !== undefined) cb(new Error(error));
|
||||
}
|
||||
|
||||
|
||||
}).fail(error => {
|
||||
this.setState({exporting: false, error: (error.responseJSON || {})[0] || JSON.stringify(error)});
|
||||
error = (error.responseJSON || {})[0] || JSON.stringify(error);
|
||||
this.setState({exporting: false, error});
|
||||
if (cb !== undefined) cb(new Error(error));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +150,7 @@ export default class ExportAssetPanel extends React.Component {
|
|||
}
|
||||
|
||||
render(){
|
||||
const {epsg, customEpsg, exporting} = this.state;
|
||||
const {epsg, customEpsg, exporting, format } = this.state;
|
||||
const { exportFormats } = this.props;
|
||||
const utmEPSG = this.props.task.epsg;
|
||||
|
||||
|
@ -162,30 +177,43 @@ export default class ExportAssetPanel extends React.Component {
|
|||
: ""}
|
||||
</div>) : "";
|
||||
|
||||
return (<div className="export-asset-panel">
|
||||
<ErrorMessage bind={[this, "error"]} />
|
||||
|
||||
{projection}
|
||||
|
||||
<div className="row form-group form-inline">
|
||||
<label className="col-sm-3 control-label">{_("Export:")}</label>
|
||||
<div className="col-sm-9">
|
||||
<div className={"btn-group " + (this.props.dropUp ? "dropup" : "")}>
|
||||
<button onClick={this.handleExport(exportFormats[0])}
|
||||
disabled={disabled} type="button" className="btn btn-sm btn-primary btn-export">
|
||||
{exporting ? <i className="fa fa-spin fa-circle-notch"/> : <i className={this.efInfo[exportFormats[0]].icon + " fa-fw"}/>} {exporting ? _("Exporting...") : this.efInfo[exportFormats[0]].label}
|
||||
</button>
|
||||
<button disabled={disabled} type="button" className="btn btn-sm dropdown-toggle btn-primary" data-toggle="dropdown"><span className="caret"></span></button>
|
||||
<ul className="dropdown-menu pull-right">
|
||||
{exportFormats.map(ef => <li key={ef}>
|
||||
<a href="javascript:void(0);" onClick={this.handleExport(ef)}>
|
||||
<i className={this.efInfo[ef].icon + " fa-fw"}></i> {this.efInfo[ef].label}
|
||||
</a>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
let exportSelector = null;
|
||||
if (this.props.selectorOnly){
|
||||
exportSelector = (<div className="row form-group form-inline">
|
||||
<label className="col-sm-3 control-label">{_("Format:")}</label>
|
||||
<div className="col-sm-9 ">
|
||||
<select className="form-control" value={format} onChange={this.handleSelectFormat}>
|
||||
{exportFormats.map(ef => <option key={ef} value={ef}>{this.efInfo[ef].label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>);
|
||||
}else{
|
||||
exportSelector = (<div className="row form-group form-inline">
|
||||
<label className="col-sm-3 control-label">{_("Export:")}</label>
|
||||
<div className="col-sm-9">
|
||||
<div className={"btn-group " + (this.props.dropUp ? "dropup" : "")}>
|
||||
<button onClick={this.handleExport(exportFormats[0])}
|
||||
disabled={disabled} type="button" className="btn btn-sm btn-primary btn-export">
|
||||
{exporting ? <i className="fa fa-spin fa-circle-notch"/> : <i className={this.efInfo[exportFormats[0]].icon + " fa-fw"}/>} {exporting ? _("Exporting...") : this.efInfo[exportFormats[0]].label}
|
||||
</button>
|
||||
<button disabled={disabled} type="button" className="btn btn-sm dropdown-toggle btn-primary" data-toggle="dropdown"><span className="caret"></span></button>
|
||||
<ul className="dropdown-menu pull-right">
|
||||
{exportFormats.map(ef => <li key={ef}>
|
||||
<a href="javascript:void(0);" onClick={this.handleExport(ef)}>
|
||||
<i className={this.efInfo[ef].icon + " fa-fw"}></i> {this.efInfo[ef].label}
|
||||
</a>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (<div className="export-asset-panel">
|
||||
{!this.props.selectorOnly ? <ErrorMessage bind={[this, "error"]} /> : ""}
|
||||
|
||||
{projection}
|
||||
{exportSelector}
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ class FormDialog extends React.Component {
|
|||
getFormData: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func,
|
||||
saveAction: PropTypes.func.isRequired,
|
||||
handleSaveFunction: PropTypes.func,
|
||||
onShow: PropTypes.func,
|
||||
onHide: PropTypes.func,
|
||||
deleteAction: PropTypes.func,
|
||||
|
@ -104,25 +105,35 @@ class FormDialog extends React.Component {
|
|||
handleSave(e){
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({saving: true});
|
||||
this.setState({saving: true, error: ""});
|
||||
|
||||
let formData = {};
|
||||
if (this.props.getFormData) formData = this.props.getFormData();
|
||||
|
||||
this.serverRequest = this.props.saveAction(formData);
|
||||
if (this.serverRequest){
|
||||
this.serverRequest.fail(e => {
|
||||
this.setState({error: e.message || (e.responseJSON || {}).detail || (e.responseJSON || {}).error || e.responseText || _("Could not apply changes")});
|
||||
}).always(() => {
|
||||
this.setState({saving: false});
|
||||
this.serverRequest = null;
|
||||
}).done(() => {
|
||||
this.hide();
|
||||
if (this.props.handleSaveFunction){
|
||||
this.props.handleSaveFunction(err => {
|
||||
if (!err) this.hide();
|
||||
else{
|
||||
this.setState({saving: false, error: err.message});
|
||||
}
|
||||
});
|
||||
}else{
|
||||
this.setState({saving: false});
|
||||
this.hide();
|
||||
let formData = {};
|
||||
if (this.props.getFormData) formData = this.props.getFormData();
|
||||
|
||||
this.serverRequest = this.props.saveAction(formData);
|
||||
if (this.serverRequest){
|
||||
this.serverRequest.fail(e => {
|
||||
this.setState({error: e.message || (e.responseJSON || {}).detail || (e.responseJSON || {}).error || e.responseText || _("Could not apply changes")});
|
||||
}).always(() => {
|
||||
this.setState({saving: false});
|
||||
this.serverRequest = null;
|
||||
}).done(() => {
|
||||
this.hide();
|
||||
});
|
||||
}else{
|
||||
this.setState({saving: false});
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handleDelete(){
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
.export-asset-dialog{
|
||||
}
|
Ładowanie…
Reference in New Issue