diff --git a/app/api/tasks.py b/app/api/tasks.py index eb2866ee..ba91f639 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -286,18 +286,18 @@ class TaskNestedView(APIView): return task -class TaskTiles(TaskNestedView): - def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y=""): - """ - Get a tile image - """ - task = self.get_and_check_task(request, pk) - tile_path = task.get_tile_path(tile_type, z, x, y) - if os.path.isfile(tile_path): - tile = open(tile_path, "rb") - return HttpResponse(FileWrapper(tile), content_type="image/png") - else: - raise exceptions.NotFound() +# class TaskTiles(TaskNestedView): +# def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y=""): +# """ +# Get a tile image +# """ +# task = self.get_and_check_task(request, pk) +# tile_path = task.get_tile_path(tile_type, z, x, y) +# if os.path.isfile(tile_path): +# tile = open(tile_path, "rb") +# return HttpResponse(FileWrapper(tile), content_type="image/png") +# else: +# raise exceptions.NotFound() def download_file_response(request, filePath, content_disposition): diff --git a/app/api/tiler.py b/app/api/tiler.py index 1b5088cb..3dbb87da 100644 --- a/app/api/tiler.py +++ b/app/api/tiler.py @@ -1,10 +1,71 @@ import rasterio +from django.http import HttpResponse +from rio_tiler.errors import TileOutsideBounds 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 rest_framework import exceptions 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): 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) - 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)) - - raster_path = task.get_asset_download_path(tile_type + ".tif") + raster_path = get_raster_path(task, tile_type) with rasterio.open(raster_path) as src_dst: minzoom, maxzoom = get_zooms(src_dst) @@ -34,13 +81,91 @@ class TileJson(TaskNestedView): 'tilejson': '2.1.0', 'name': task.name, 'version': '1.0.0', - 'scheme': 'tms', - 'tiles': ['/api/projects/{}/tasks/{}/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id, tile_type)], + 'scheme': 'xyz', + 'tiles': [get_tile_url(task, tile_type)], 'minzoom': minzoom, '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) + ) \ No newline at end of file diff --git a/app/api/urls.py b/app/api/urls.py index 78207f22..ce2a4fae 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -3,12 +3,12 @@ from django.conf.urls import url, include from app.api.presets import PresetViewSet from app.plugins import get_api_url_patterns 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 .admin import UserViewSet, GroupViewSet from rest_framework_nested import routers from rest_framework_jwt.views import obtain_jwt_token -from .tiler import TileJson +from .tiler import TileJson, Bounds, Metadata, Tiles router = routers.DefaultRouter() router.register(r'projects', ProjectViewSet) @@ -30,9 +30,12 @@ urlpatterns = [ url(r'^', include(tasks_router.urls)), url(r'^', include(admin_router.urls)), - url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/(?Porthophoto|dsm|dtm)/tiles/(?P[\d]+)/(?P[\d]+)/(?P[\d]+)\.png$', TaskTiles.as_view()), url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/(?Porthophoto|dsm|dtm)/tiles\.json$', TileJson.as_view()), + url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/(?Porthophoto|dsm|dtm)/bounds$', Bounds.as_view()), + url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/(?Porthophoto|dsm|dtm)/metadata$', Metadata.as_view()), + url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/(?Porthophoto|dsm|dtm)/tiles/(?P[\d]+)/(?P[\d]+)/(?P[\d]+)\.png$', Tiles.as_view()), + url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/(?Porthophoto|dsm|dtm)/tiles/(?P[\d]+)/(?P[\d]+)/(?P[\d]+)@(?P[\d]+)x\.png$', Tiles.as_view()), url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/download/(?P.+)$', TaskDownloads.as_view()), url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/assets/(?P.+)$', TaskAssets.as_view()), diff --git a/app/plugins/functions.py b/app/plugins/functions.py index 82eb4d9e..3910624d 100644 --- a/app/plugins/functions.py +++ b/app/plugins/functions.py @@ -100,7 +100,7 @@ def build_plugins(): # Check for webpack.config.js (if we need to build it) 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())) subprocess.Popen(['webpack-cli', '--watch'], cwd=plugin.get_path("public")) elif not plugin.path_exists("public/build"): @@ -108,6 +108,22 @@ def build_plugins(): 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(): for plugin in get_active_plugins(): try: diff --git a/app/plugins/templates/webpack.config.js.tmpl b/app/plugins/templates/webpack.config.js.tmpl index 2443bafb..0817dc92 100644 --- a/app/plugins/templates/webpack.config.js.tmpl +++ b/app/plugins/templates/webpack.config.js.tmpl @@ -85,7 +85,7 @@ module.exports = { }, watchOptions: { - ignored: /node_modules/, + ignored: ['node_modules', './**/*.py'], aggregateTimeout: 300, poll: 1000 } diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index 5303fe33..464abc2b 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -22,8 +22,6 @@ import update from 'immutability-helper'; class Map extends React.Component { static defaultProps = { - maxzoom: 18, - minzoom: 0, showBackground: false, opacity: 100, mapType: "orthophoto", @@ -31,8 +29,6 @@ class Map extends React.Component { }; static propTypes = { - maxzoom: PropTypes.number, - minzoom: PropTypes.number, showBackground: PropTypes.bool, tiles: PropTypes.array.isRequired, opacity: PropTypes.number, @@ -97,9 +93,9 @@ class Map extends React.Component { ); const layer = Leaflet.tileLayer(info.tiles[0], { bounds, - minZoom: info.minzoom, - maxZoom: L.Browser.retina ? (info.maxzoom + 1) : info.maxzoom, - maxNativeZoom: L.Browser.retina ? (info.maxzoom - 1) : info.maxzoom, + minZoom: 0, + maxZoom: info.maxzoom + 4, + maxNativeZoom: info.maxzoom, tms: info.scheme === 'tms', opacity: this.props.opacity / 100, detectRetina: true @@ -203,7 +199,9 @@ class Map extends React.Component { this.map = Leaflet.map(this.container, { scrollWheelZoom: true, positionControl: true, - zoomControl: false + zoomControl: false, + minZoom: 0, + maxZoom: 24 }); PluginsAPI.Map.triggerWillAddControls({ @@ -247,7 +245,7 @@ https://a.tile.openstreetmap.org/{z}/{x}/{y}.png if (url){ customLayer.clearLayers(); const l = L.tileLayer(url, { - maxZoom: 21, + maxZoom: 24, minZoom: 0 }); customLayer.addLayer(l); diff --git a/plugins/elevationmap/requirements.txt b/plugins/elevationmap/requirements.txt index ce208f40..24add85a 100644 --- a/plugins/elevationmap/requirements.txt +++ b/plugins/elevationmap/requirements.txt @@ -1,3 +1,2 @@ geojson==2.4.1 opencv-python==4.1.0.25 -rasterio==1.0.23 diff --git a/requirements.txt b/requirements.txt index f8c1df79..7522556a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -54,4 +54,5 @@ uritemplate==3.0.0 vine==1.1.4 webcolors==1.5 rasterio==1.1.0 -rio-tiler==1.3.0 \ No newline at end of file +rio-tiler==1.3.0 +rio-color==1.0.0 \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 04a2a1fc..752f77ee 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -93,7 +93,7 @@ module.exports = { }, watchOptions: { - ignored: /node_modules/, + ignored: ['node_modules', './**/*.py'], aggregateTimeout: 300, poll: 1000 }