KMZ export support

pull/1086/head
Piero Toffanin 2021-11-02 18:15:51 -04:00
rodzic dd3b413401
commit 8054b650f9
4 zmienionych plików z 64 dodań i 21 usunięć

Wyświetl plik

@ -246,26 +246,26 @@ def get_elevation_tiles(elevation, url, x, y, z, tilesize, nodata, resampling, p
tile = np.full((tilesize * 3, tilesize * 3), nodata, dtype=elevation.dtype) tile = np.full((tilesize * 3, tilesize * 3), nodata, dtype=elevation.dtype)
with COGReader(url) as src: with COGReader(url) as src:
try: try:
left, _ = src.tile(x - 1, y, z, indexes=1, tilesize=tilesize, nodata=nodata, left, _discard_ = src.tile(x - 1, y, z, indexes=1, tilesize=tilesize, nodata=nodata,
resampling_method=resampling, padding=padding) resampling_method=resampling, padding=padding)
tile[tilesize:tilesize * 2, 0:tilesize] = left tile[tilesize:tilesize * 2, 0:tilesize] = left
except TileOutsideBounds: except TileOutsideBounds:
pass pass
try: try:
right, _ = src.tile(x + 1, y, z, indexes=1, tilesize=tilesize, nodata=nodata, right, _discard_ = src.tile(x + 1, y, z, indexes=1, tilesize=tilesize, nodata=nodata,
resampling_method=resampling, padding=padding) resampling_method=resampling, padding=padding)
tile[tilesize:tilesize * 2, tilesize * 2:tilesize * 3] = right tile[tilesize:tilesize * 2, tilesize * 2:tilesize * 3] = right
except TileOutsideBounds: except TileOutsideBounds:
pass pass
try: try:
bottom, _ = src.tile(x, y + 1, z, indexes=1, tilesize=tilesize, nodata=nodata, bottom, _discard_ = src.tile(x, y + 1, z, indexes=1, tilesize=tilesize, nodata=nodata,
resampling_method=resampling, padding=padding) resampling_method=resampling, padding=padding)
tile[tilesize * 2:tilesize * 3, tilesize:tilesize * 2] = bottom tile[tilesize * 2:tilesize * 3, tilesize:tilesize * 2] = bottom
except TileOutsideBounds: except TileOutsideBounds:
pass pass
try: try:
top, _ = src.tile(x, y - 1, z, indexes=1, tilesize=tilesize, nodata=nodata, top, _discard_ = src.tile(x, y - 1, z, indexes=1, tilesize=tilesize, nodata=nodata,
resampling_method=resampling, padding=padding) resampling_method=resampling, padding=padding)
tile[0:tilesize, tilesize:tilesize * 2] = top tile[0:tilesize, tilesize:tilesize * 2] = top
except TileOutsideBounds: except TileOutsideBounds:
@ -314,7 +314,7 @@ class Tiles(TaskNestedView):
if hillshade == '' or hillshade == '0': hillshade = None if hillshade == '' or hillshade == '0': hillshade = None
try: try:
expr, _ = lookup_formula(formula, bands) expr, _discard_ = lookup_formula(formula, bands)
except ValueError as e: except ValueError as e:
raise exceptions.ValidationError(str(e)) raise exceptions.ValidationError(str(e))
@ -442,7 +442,7 @@ class Tiles(TaskNestedView):
if intensity is not None: if intensity is not None:
rgb = tile.post_process(in_range=(rescale_arr,)) rgb = tile.post_process(in_range=(rescale_arr,))
if colormap: if colormap:
rgb, _ = apply_cmap(rgb.data, colormap.get(color_map)) rgb, _discard_ = apply_cmap(rgb.data, colormap.get(color_map))
if rgb.data.shape[0] != 3: if rgb.data.shape[0] != 3:
raise exceptions.ValidationError( raise exceptions.ValidationError(
_("Cannot process tile: intensity image provided, but no RGB data was computed.")) _("Cannot process tile: intensity image provided, but no RGB data was computed."))
@ -486,21 +486,23 @@ class Export(TaskNestedView):
if bands == '': bands = None if bands == '': bands = None
if rescale == '': rescale = None if rescale == '': rescale = None
if epsg == '': epsg = None if epsg == '': epsg = None
if color_map == '': color_map = None
if hillshade == '': hillshade = None
expr = None expr = None
if not export_format in ['gtiff', 'gtiff-rgb', 'jpg', 'png']: if not export_format in ['gtiff', 'gtiff-rgb', 'jpg', 'png', 'kmz']:
raise exceptions.ValidationError(_("Unsupported format: %(format)") % {'format': export_format}) raise exceptions.ValidationError(_("Unsupported format: %(value)s") % {'value': export_format})
if epsg is not None: if epsg is not None:
try: try:
epsg = int(epsg) epsg = int(epsg)
except ValueError: except ValueError:
raise exception.ValidationError(_("Invalid EPSG code: %(epsg)") % {'epsg': epsg}) raise exceptions.ValidationError(_("Invalid EPSG code: %(value)s") % {'value': epsg})
if formula and bands: if formula and bands:
try: try:
expr, _ = lookup_formula(formula, bands) expr, _discard_ = lookup_formula(formula, bands)
except ValueError as e: except ValueError as e:
raise exceptions.ValidationError(str(e)) raise exceptions.ValidationError(str(e))
@ -516,7 +518,7 @@ class Export(TaskNestedView):
try: try:
rescale = list(map(float, rescale.split(","))) rescale = list(map(float, rescale.split(",")))
except ValueError: except ValueError:
raise exception.ValidationError(_("Invalid rescale value: %(value)") % {'value': rescale}) raise exceptions.ValidationError(_("Invalid rescale value: %(value)s") % {'value': rescale})
if hillshade is not None: if hillshade is not None:
try: try:
@ -524,7 +526,7 @@ class Export(TaskNestedView):
if hillshade < 0: if hillshade < 0:
raise Exception("Hillshade must be > 0") raise Exception("Hillshade must be > 0")
except: except:
raise exception.ValidationError(_("Invalid hillshade value: %(value)") % {'value': hillshade}) raise exceptions.ValidationError(_("Invalid hillshade value: %(value)s") % {'value': hillshade})
url = get_raster_path(task, asset_type) url = get_raster_path(task, asset_type)
@ -549,5 +551,7 @@ class Export(TaskNestedView):
rescale=rescale, rescale=rescale,
color_map=color_map, color_map=color_map,
hillshade=hillshade, hillshade=hillshade,
dem=asset_type in ['dsm', 'dtm']).task_id dem=asset_type in ['dsm', 'dtm'],
name=task.name,
asset_type=asset_type).task_id
return Response({'celery_task_id': celery_task_id, 'filename': filename}) return Response({'celery_task_id': celery_task_id, 'filename': filename})

Wyświetl plik

@ -1,6 +1,9 @@
# Export a raster index after applying a band expression # Export a raster index after applying a band expression
import rasterio import rasterio
import re import re
import logging
import os
import subprocess
import numpy as np import numpy as np
import numexpr as ne import numexpr as ne
from rasterio.enums import ColorInterp from rasterio.enums import ColorInterp
@ -10,7 +13,6 @@ from rio_tiler.errors import InvalidColorMapName
from app.api.hsvblend import hsv_blend from app.api.hsvblend import hsv_blend
from app.api.hillshade import LightSource from app.api.hillshade import LightSource
from rasterio.warp import calculate_default_transform, reproject, Resampling from rasterio.warp import calculate_default_transform, reproject, Resampling
import logging
logger = logging.getLogger('app.logger') logger = logging.getLogger('app.logger')
@ -21,7 +23,8 @@ def extension_for_export_format(export_format):
'gtiff': 'tif', 'gtiff': 'tif',
'gtiff-rgb': 'tif', 'gtiff-rgb': 'tif',
'jpg': 'jpg', 'jpg': 'jpg',
'png': 'png' 'png': 'png',
'kmz': 'kmz'
} }
return extensions.get(export_format, 'tif') return extensions.get(export_format, 'tif')
@ -33,6 +36,8 @@ def export_raster(input, output, **opts):
color_map = opts.get('color_map') color_map = opts.get('color_map')
hillshade = opts.get('hillshade') hillshade = opts.get('hillshade')
dem = opts.get('dem') dem = opts.get('dem')
name = opts.get('name', 'raster') # KMZ specific
asset_type = opts.get('asset_type', 'raster') # KMZ specific
with rasterio.open(input) as src: with rasterio.open(input) as src:
profile = src.meta.copy() profile = src.meta.copy()
@ -43,6 +48,18 @@ def export_raster(input, output, **opts):
with_alpha = True with_alpha = True
rgb = False rgb = False
indexes = src.indexes indexes = src.indexes
output_raster = output
jpg_background = 255 # white
# KMZ is special, we just export it as jpg with EPSG:4326
# and then call GDAL to tile/package it
kmz = export_format == "kmz"
if kmz:
export_format = "jpg"
epsg = 4326
path_base, _ = os.path.splitext(output)
output_raster = path_base + ".jpg"
jpg_background = 0 # black
if export_format == "jpg": if export_format == "jpg":
driver = "JPEG" driver = "JPEG"
@ -95,7 +112,7 @@ def export_raster(input, output, **opts):
if not skip_rescale and rescale is not None: if not skip_rescale and rescale is not None:
arr = linear_rescale(arr, in_range=rescale) arr = linear_rescale(arr, in_range=rescale)
if not skip_alpha and not with_alpha: if not skip_alpha and not with_alpha:
arr[mask==0] = 255 # Set white background arr[mask==0] = jpg_background
if not skip_type and rgb and arr.dtype != np.uint8: if not skip_type and rgb and arr.dtype != np.uint8:
arr = arr.astype(np.uint8) arr = arr.astype(np.uint8)
@ -173,7 +190,7 @@ def export_raster(input, output, **opts):
# Make sure this is float32 # Make sure this is float32
arr = arr.astype(np.float32) arr = arr.astype(np.float32)
with rasterio.open(output, 'w', **profile) as dst: with rasterio.open(output_raster, 'w', **profile) as dst:
# Apply colormap? # Apply colormap?
if rgb and cmap is not None: if rgb and cmap is not None:
rgb_data, _ = apply_cmap(process(arr, skip_alpha=True), cmap) rgb_data, _ = apply_cmap(process(arr, skip_alpha=True), cmap)
@ -190,7 +207,7 @@ def export_raster(input, output, **opts):
write_band(process(arr)[0], dst, 1) write_band(process(arr)[0], dst, 1)
elif dem: elif dem:
# Apply hillshading, colormaps to elevation # Apply hillshading, colormaps to elevation
with rasterio.open(output, 'w', **profile) as dst: with rasterio.open(output_raster, 'w', **profile) as dst:
arr = src.read() arr = src.read()
intensity = None intensity = None
@ -221,7 +238,7 @@ def export_raster(input, output, **opts):
write_band(process(arr)[0], dst, 1) write_band(process(arr)[0], dst, 1)
else: else:
# Copy bands as-is # Copy bands as-is
with rasterio.open(output, 'w', **profile) as dst: with rasterio.open(output_raster, 'w', **profile) as dst:
band_num = 1 band_num = 1
for idx in indexes: for idx in indexes:
ci = src.colorinterp[idx - 1] ci = src.colorinterp[idx - 1]
@ -234,3 +251,8 @@ def export_raster(input, output, **opts):
else: else:
write_band(process(arr), dst, band_num) write_band(process(arr), dst, band_num)
band_num += 1 band_num += 1
if kmz:
subprocess.check_output(["gdal_translate", "-of", "KMLSUPEROVERLAY",
"-co", "Name={}".format(name),
"-co", "FORMAT=JPEG", output_raster, output])

Wyświetl plik

@ -1,4 +1,5 @@
from django.core.exceptions import SuspiciousFileOperation from django.core.exceptions import SuspiciousFileOperation
from shlex import _find_unsafe
import os import os
def path_traversal_check(unsafe_path, known_safe_path): def path_traversal_check(unsafe_path, known_safe_path):
@ -9,4 +10,16 @@ def path_traversal_check(unsafe_path, known_safe_path):
raise SuspiciousFileOperation("{} is not safe".format(unsafe_path)) raise SuspiciousFileOperation("{} is not safe".format(unsafe_path))
# Passes the check # Passes the check
return unsafe_path return unsafe_path
def double_quote(s):
"""Return a shell-escaped version of the string *s*."""
if not s:
return '""'
if _find_unsafe(s) is None:
return s
# use double quotes, and prefix double quotes with a \
# the string $"b is then quoted as "$\"b"
return '"' + s.replace('"', '\\\"') + '"'

Wyświetl plik

@ -9,7 +9,7 @@ import Workers from '../classes/Workers';
export default class ExportAssetPanel extends React.Component { export default class ExportAssetPanel extends React.Component {
static defaultProps = { static defaultProps = {
exportFormats: ["gtiff-rgb", "gtiff", "jpg", "png"], exportFormats: ["gtiff-rgb", "gtiff", "jpg", "png", "kmz"],
asset: "", asset: "",
exportParams: {}, exportParams: {},
task: null, task: null,
@ -45,6 +45,10 @@ export default class ExportAssetPanel extends React.Component {
'png': { 'png': {
label: _("PNG (RGB)"), label: _("PNG (RGB)"),
icon: "fas fa-palette" icon: "fas fa-palette"
},
'kmz': {
label: _("KMZ (RGB)"),
icon: "fa fa-globe"
} }
}; };