kopia lustrzana https://github.com/OpenDroneMap/WebODM
COG streaming API (WIP)
rodzic
a4a6f273be
commit
6479d564ba
|
@ -286,18 +286,18 @@ class TaskNestedView(APIView):
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
|
||||||
class TaskTiles(TaskNestedView):
|
# class TaskTiles(TaskNestedView):
|
||||||
def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y=""):
|
# def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y=""):
|
||||||
"""
|
# """
|
||||||
Get a tile image
|
# Get a tile image
|
||||||
"""
|
# """
|
||||||
task = self.get_and_check_task(request, pk)
|
# task = self.get_and_check_task(request, pk)
|
||||||
tile_path = task.get_tile_path(tile_type, z, x, y)
|
# tile_path = task.get_tile_path(tile_type, z, x, y)
|
||||||
if os.path.isfile(tile_path):
|
# if os.path.isfile(tile_path):
|
||||||
tile = open(tile_path, "rb")
|
# tile = open(tile_path, "rb")
|
||||||
return HttpResponse(FileWrapper(tile), content_type="image/png")
|
# return HttpResponse(FileWrapper(tile), content_type="image/png")
|
||||||
else:
|
# else:
|
||||||
raise exceptions.NotFound()
|
# raise exceptions.NotFound()
|
||||||
|
|
||||||
|
|
||||||
def download_file_response(request, filePath, content_disposition):
|
def download_file_response(request, filePath, content_disposition):
|
||||||
|
|
161
app/api/tiler.py
161
app/api/tiler.py
|
@ -1,10 +1,71 @@
|
||||||
import rasterio
|
import rasterio
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from rio_tiler.errors import TileOutsideBounds
|
||||||
from rio_tiler.mercator import get_zooms
|
from rio_tiler.mercator import get_zooms
|
||||||
|
from rio_tiler import main
|
||||||
|
from rio_tiler.utils import array_to_image, get_colormap, expression, linear_rescale, _chunks
|
||||||
|
from rio_color.operations import parse_operations
|
||||||
|
from rio_color.utils import scale_dtype, to_math_type
|
||||||
|
from rio_tiler.profiles import img_profiles
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
from .tasks import TaskNestedView
|
from .tasks import TaskNestedView
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
def get_tile_url(task, tile_type):
|
||||||
|
return '/api/projects/{}/tasks/{}/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id, tile_type)
|
||||||
|
|
||||||
|
def get_extent(task, tile_type):
|
||||||
|
extent_map = {
|
||||||
|
'orthophoto': task.orthophoto_extent,
|
||||||
|
'dsm': task.dsm_extent,
|
||||||
|
'dtm': task.dtm_extent,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not tile_type in extent_map:
|
||||||
|
raise exceptions.ValidationError("Type {} is not a valid tile type".format(tile_type))
|
||||||
|
|
||||||
|
extent = extent_map[tile_type]
|
||||||
|
|
||||||
|
if extent is None:
|
||||||
|
raise exceptions.ValidationError(
|
||||||
|
"A {} has not been processed for this task. Tiles are not available.".format(tile_type))
|
||||||
|
|
||||||
|
return extent.extent
|
||||||
|
|
||||||
|
def get_raster_path(task, tile_type):
|
||||||
|
return task.get_asset_download_path(tile_type + ".tif")
|
||||||
|
|
||||||
|
|
||||||
|
def postprocess(tile, mask, rescale = None, color_formula = None):
|
||||||
|
if rescale:
|
||||||
|
rescale_arr = list(map(float, rescale.split(",")))
|
||||||
|
rescale_arr = list(_chunks(rescale_arr, 2))
|
||||||
|
if len(rescale_arr) != tile.shape[0]:
|
||||||
|
rescale_arr = ((rescale_arr[0]),) * tile.shape[0]
|
||||||
|
for bdx in range(tile.shape[0]):
|
||||||
|
tile[bdx] = numpy.where(
|
||||||
|
mask,
|
||||||
|
linear_rescale(
|
||||||
|
tile[bdx], in_range=rescale_arr[bdx], out_range=[0, 255]
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
tile = tile.astype(numpy.uint8)
|
||||||
|
|
||||||
|
if color_formula:
|
||||||
|
# make sure one last time we don't have
|
||||||
|
# negative value before applying color formula
|
||||||
|
tile[tile < 0] = 0
|
||||||
|
for ops in parse_operations(color_formula):
|
||||||
|
tile = scale_dtype(ops(to_math_type(tile)), numpy.uint8)
|
||||||
|
|
||||||
|
return tile, mask
|
||||||
|
|
||||||
|
|
||||||
class TileJson(TaskNestedView):
|
class TileJson(TaskNestedView):
|
||||||
def get(self, request, pk=None, project_pk=None, tile_type=""):
|
def get(self, request, pk=None, project_pk=None, tile_type=""):
|
||||||
"""
|
"""
|
||||||
|
@ -12,21 +73,7 @@ class TileJson(TaskNestedView):
|
||||||
"""
|
"""
|
||||||
task = self.get_and_check_task(request, pk)
|
task = self.get_and_check_task(request, pk)
|
||||||
|
|
||||||
extent_map = {
|
raster_path = get_raster_path(task, tile_type)
|
||||||
'orthophoto': task.orthophoto_extent,
|
|
||||||
'dsm': task.dsm_extent,
|
|
||||||
'dtm': task.dtm_extent,
|
|
||||||
}
|
|
||||||
|
|
||||||
if not tile_type in extent_map:
|
|
||||||
raise exceptions.ValidationError("Type {} is not a valid tile type".format(tile_type))
|
|
||||||
|
|
||||||
extent = extent_map[tile_type]
|
|
||||||
|
|
||||||
if extent is None:
|
|
||||||
raise exceptions.ValidationError("A {} has not been processed for this task. Tiles are not available.".format(tile_type))
|
|
||||||
|
|
||||||
raster_path = task.get_asset_download_path(tile_type + ".tif")
|
|
||||||
with rasterio.open(raster_path) as src_dst:
|
with rasterio.open(raster_path) as src_dst:
|
||||||
minzoom, maxzoom = get_zooms(src_dst)
|
minzoom, maxzoom = get_zooms(src_dst)
|
||||||
|
|
||||||
|
@ -34,13 +81,91 @@ class TileJson(TaskNestedView):
|
||||||
'tilejson': '2.1.0',
|
'tilejson': '2.1.0',
|
||||||
'name': task.name,
|
'name': task.name,
|
||||||
'version': '1.0.0',
|
'version': '1.0.0',
|
||||||
'scheme': 'tms',
|
'scheme': 'xyz',
|
||||||
'tiles': ['/api/projects/{}/tasks/{}/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id, tile_type)],
|
'tiles': [get_tile_url(task, tile_type)],
|
||||||
'minzoom': minzoom,
|
'minzoom': minzoom,
|
||||||
'maxzoom': maxzoom,
|
'maxzoom': maxzoom,
|
||||||
'bounds': extent.extent
|
'bounds': get_extent(task, tile_type)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
class Bounds(TaskNestedView):
|
||||||
|
def get(self, request, pk=None, project_pk=None, tile_type=""):
|
||||||
|
"""
|
||||||
|
Get the bounds for this tasks's asset type
|
||||||
|
"""
|
||||||
|
task = self.get_and_check_task(request, pk)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'url': get_tile_url(task, tile_type),
|
||||||
|
'bounds': get_extent(task, tile_type)
|
||||||
|
})
|
||||||
|
|
||||||
|
class Metadata(TaskNestedView):
|
||||||
|
def get(self, request, pk=None, project_pk=None, tile_type=""):
|
||||||
|
"""
|
||||||
|
Get the metadata for this tasks's asset type
|
||||||
|
"""
|
||||||
|
task = self.get_and_check_task(request, pk)
|
||||||
|
|
||||||
|
raster_path = get_raster_path(task, tile_type)
|
||||||
|
info = main.metadata(raster_path, pmin=2.0, pmax=98.0)
|
||||||
|
info['address'] = get_tile_url(task, tile_type)
|
||||||
|
return Response(info)
|
||||||
|
|
||||||
|
class Tiles(TaskNestedView):
|
||||||
|
def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y="", scale=1):
|
||||||
|
"""
|
||||||
|
Get a tile image
|
||||||
|
"""
|
||||||
|
task = self.get_and_check_task(request, pk)
|
||||||
|
|
||||||
|
z = int(z)
|
||||||
|
x = int(x)
|
||||||
|
y = int(y)
|
||||||
|
scale = int(scale)
|
||||||
|
ext = "png"
|
||||||
|
driver = "jpeg" if ext == "jpg" else ext
|
||||||
|
|
||||||
|
indexes = self.request.query_params.get('indexes')
|
||||||
|
expr = self.request.query_params.get('expr')
|
||||||
|
rescale = self.request.query_params.get('rescale')
|
||||||
|
color_formula = self.request.query_params.get('color_formula')
|
||||||
|
color_map = self.request.query_params.get('color_map')
|
||||||
|
nodata = self.request.query_params.get('nodata')
|
||||||
|
|
||||||
|
if tile_type in ['dsm', 'dtm'] and rescale is None:
|
||||||
|
#raise exceptions.ValidationError("Cannot get tiles without rescale parameter. Add ?rescale=min,max to the URL.")
|
||||||
|
|
||||||
|
if rescale is None:
|
||||||
|
rescale = '157.0500,164.850'
|
||||||
|
|
||||||
|
if nodata is not None:
|
||||||
|
nodata = numpy.nan if nodata == "nan" else float(nodata)
|
||||||
|
tilesize = scale * 256
|
||||||
|
|
||||||
|
url = get_raster_path(task, tile_type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if expr is not None:
|
||||||
|
tile, mask = expression(
|
||||||
|
url, x, y, z, expr=expr, tilesize=tilesize, nodata=nodata
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
tile, mask = main.tile(
|
||||||
|
url, x, y, z, indexes=indexes, tilesize=tilesize, nodata=nodata
|
||||||
|
)
|
||||||
|
except TileOutsideBounds:
|
||||||
|
raise exceptions.NotFound("Outside of bounds")
|
||||||
|
|
||||||
|
rtile, rmask = postprocess(
|
||||||
|
tile, mask, rescale=rescale, color_formula=color_formula
|
||||||
|
)
|
||||||
|
|
||||||
|
if color_map:
|
||||||
|
color_map = get_colormap(color_map, format="gdal")
|
||||||
|
|
||||||
|
options = img_profiles.get(driver, {})
|
||||||
|
return HttpResponse(
|
||||||
|
array_to_image(rtile, rmask, img_format=driver, color_map=color_map, **options),
|
||||||
|
content_type="image/{}".format(ext)
|
||||||
|
)
|
|
@ -3,12 +3,12 @@ from django.conf.urls import url, include
|
||||||
from app.api.presets import PresetViewSet
|
from app.api.presets import PresetViewSet
|
||||||
from app.plugins import get_api_url_patterns
|
from app.plugins import get_api_url_patterns
|
||||||
from .projects import ProjectViewSet
|
from .projects import ProjectViewSet
|
||||||
from .tasks import TaskViewSet, TaskTiles, TaskDownloads, TaskAssets, TaskAssetsImport
|
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskAssetsImport
|
||||||
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
|
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
|
||||||
from .admin import UserViewSet, GroupViewSet
|
from .admin import UserViewSet, GroupViewSet
|
||||||
from rest_framework_nested import routers
|
from rest_framework_nested import routers
|
||||||
from rest_framework_jwt.views import obtain_jwt_token
|
from rest_framework_jwt.views import obtain_jwt_token
|
||||||
from .tiler import TileJson
|
from .tiler import TileJson, Bounds, Metadata, Tiles
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register(r'projects', ProjectViewSet)
|
router.register(r'projects', ProjectViewSet)
|
||||||
|
@ -30,9 +30,12 @@ urlpatterns = [
|
||||||
url(r'^', include(tasks_router.urls)),
|
url(r'^', include(tasks_router.urls)),
|
||||||
url(r'^', include(admin_router.urls)),
|
url(r'^', include(admin_router.urls)),
|
||||||
|
|
||||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)\.png$', TaskTiles.as_view()),
|
|
||||||
|
|
||||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles\.json$', TileJson.as_view()),
|
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles\.json$', TileJson.as_view()),
|
||||||
|
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/bounds$', Bounds.as_view()),
|
||||||
|
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/metadata$', Metadata.as_view()),
|
||||||
|
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)\.png$', Tiles.as_view()),
|
||||||
|
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)@(?P<scale>[\d]+)x\.png$', Tiles.as_view()),
|
||||||
|
|
||||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()),
|
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()),
|
||||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()),
|
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()),
|
||||||
|
|
|
@ -100,7 +100,7 @@ def build_plugins():
|
||||||
|
|
||||||
# Check for webpack.config.js (if we need to build it)
|
# Check for webpack.config.js (if we need to build it)
|
||||||
if plugin.path_exists("public/webpack.config.js"):
|
if plugin.path_exists("public/webpack.config.js"):
|
||||||
if settings.DEV:
|
if settings.DEV and webpack_watch_process_count() <= 2:
|
||||||
logger.info("Running webpack with watcher for {}".format(plugin.get_name()))
|
logger.info("Running webpack with watcher for {}".format(plugin.get_name()))
|
||||||
subprocess.Popen(['webpack-cli', '--watch'], cwd=plugin.get_path("public"))
|
subprocess.Popen(['webpack-cli', '--watch'], cwd=plugin.get_path("public"))
|
||||||
elif not plugin.path_exists("public/build"):
|
elif not plugin.path_exists("public/build"):
|
||||||
|
@ -108,6 +108,22 @@ def build_plugins():
|
||||||
subprocess.call(['webpack-cli'], cwd=plugin.get_path("public"))
|
subprocess.call(['webpack-cli'], cwd=plugin.get_path("public"))
|
||||||
|
|
||||||
|
|
||||||
|
def webpack_watch_process_count():
|
||||||
|
count = 0
|
||||||
|
try:
|
||||||
|
pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
|
||||||
|
for pid in pids:
|
||||||
|
try:
|
||||||
|
if "/usr/bin/webpack-cli" in open(os.path.join('/proc', pid, 'cmdline'), 'r').read().split('\0'):
|
||||||
|
count += 1
|
||||||
|
except IOError: # proc has already terminated
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
logger.warning("webpack_watch_process_count is not supported on this platform.")
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
def register_plugins():
|
def register_plugins():
|
||||||
for plugin in get_active_plugins():
|
for plugin in get_active_plugins():
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -85,7 +85,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
watchOptions: {
|
watchOptions: {
|
||||||
ignored: /node_modules/,
|
ignored: ['node_modules', './**/*.py'],
|
||||||
aggregateTimeout: 300,
|
aggregateTimeout: 300,
|
||||||
poll: 1000
|
poll: 1000
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,6 @@ import update from 'immutability-helper';
|
||||||
|
|
||||||
class Map extends React.Component {
|
class Map extends React.Component {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
maxzoom: 18,
|
|
||||||
minzoom: 0,
|
|
||||||
showBackground: false,
|
showBackground: false,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
mapType: "orthophoto",
|
mapType: "orthophoto",
|
||||||
|
@ -31,8 +29,6 @@ class Map extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
maxzoom: PropTypes.number,
|
|
||||||
minzoom: PropTypes.number,
|
|
||||||
showBackground: PropTypes.bool,
|
showBackground: PropTypes.bool,
|
||||||
tiles: PropTypes.array.isRequired,
|
tiles: PropTypes.array.isRequired,
|
||||||
opacity: PropTypes.number,
|
opacity: PropTypes.number,
|
||||||
|
@ -97,9 +93,9 @@ class Map extends React.Component {
|
||||||
);
|
);
|
||||||
const layer = Leaflet.tileLayer(info.tiles[0], {
|
const layer = Leaflet.tileLayer(info.tiles[0], {
|
||||||
bounds,
|
bounds,
|
||||||
minZoom: info.minzoom,
|
minZoom: 0,
|
||||||
maxZoom: L.Browser.retina ? (info.maxzoom + 1) : info.maxzoom,
|
maxZoom: info.maxzoom + 4,
|
||||||
maxNativeZoom: L.Browser.retina ? (info.maxzoom - 1) : info.maxzoom,
|
maxNativeZoom: info.maxzoom,
|
||||||
tms: info.scheme === 'tms',
|
tms: info.scheme === 'tms',
|
||||||
opacity: this.props.opacity / 100,
|
opacity: this.props.opacity / 100,
|
||||||
detectRetina: true
|
detectRetina: true
|
||||||
|
@ -203,7 +199,9 @@ class Map extends React.Component {
|
||||||
this.map = Leaflet.map(this.container, {
|
this.map = Leaflet.map(this.container, {
|
||||||
scrollWheelZoom: true,
|
scrollWheelZoom: true,
|
||||||
positionControl: true,
|
positionControl: true,
|
||||||
zoomControl: false
|
zoomControl: false,
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 24
|
||||||
});
|
});
|
||||||
|
|
||||||
PluginsAPI.Map.triggerWillAddControls({
|
PluginsAPI.Map.triggerWillAddControls({
|
||||||
|
@ -247,7 +245,7 @@ https://a.tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||||
if (url){
|
if (url){
|
||||||
customLayer.clearLayers();
|
customLayer.clearLayers();
|
||||||
const l = L.tileLayer(url, {
|
const l = L.tileLayer(url, {
|
||||||
maxZoom: 21,
|
maxZoom: 24,
|
||||||
minZoom: 0
|
minZoom: 0
|
||||||
});
|
});
|
||||||
customLayer.addLayer(l);
|
customLayer.addLayer(l);
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
geojson==2.4.1
|
geojson==2.4.1
|
||||||
opencv-python==4.1.0.25
|
opencv-python==4.1.0.25
|
||||||
rasterio==1.0.23
|
|
||||||
|
|
|
@ -54,4 +54,5 @@ uritemplate==3.0.0
|
||||||
vine==1.1.4
|
vine==1.1.4
|
||||||
webcolors==1.5
|
webcolors==1.5
|
||||||
rasterio==1.1.0
|
rasterio==1.1.0
|
||||||
rio-tiler==1.3.0
|
rio-tiler==1.3.0
|
||||||
|
rio-color==1.0.0
|
|
@ -93,7 +93,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
watchOptions: {
|
watchOptions: {
|
||||||
ignored: /node_modules/,
|
ignored: ['node_modules', './**/*.py'],
|
||||||
aggregateTimeout: 300,
|
aggregateTimeout: 300,
|
||||||
poll: 1000
|
poll: 1000
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue