Export from Download Assets (orthophoto) working

pull/1086/head
Piero Toffanin 2021-11-03 15:31:12 -04:00
rodzic 8054b650f9
commit 34736e63b0
8 zmienionych plików z 219 dodań i 68 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,2 @@
.export-asset-dialog{
}