diff --git a/Contributors.md b/Contributors.md deleted file mode 100644 index fc97e277..00000000 --- a/Contributors.md +++ /dev/null @@ -1 +0,0 @@ -Rumen Mitrev diff --git a/app/api/tiler.py b/app/api/tiler.py index 128d0485..10ff77a0 100644 --- a/app/api/tiler.py +++ b/app/api/tiler.py @@ -54,7 +54,11 @@ def get_raster_path(task, tile_type): def rescale_tile(tile, mask, rescale = None): if rescale: - rescale_arr = list(map(float, rescale.split(","))) + try: + rescale_arr = list(map(float, rescale.split(","))) + except ValueError: + raise exceptions.ValidationError("Invalid rescale value") + rescale_arr = list(_chunks(rescale_arr, 2)) if len(rescale_arr) != tile.shape[0]: rescale_arr = ((rescale_arr[0]),) * tile.shape[0] @@ -273,6 +277,12 @@ class Tiles(TaskNestedView): if tile_type in ['dsm', 'dtm'] and color_map is None: color_map = "gray" + if tile_type == 'orthophoto' and formula is not None: + if color_map is None: + color_map = "gray" + if rescale is None: + rescale = "-1,1" + if nodata is not None: nodata = np.nan if nodata == "nan" else float(nodata) tilesize = scale * 256 @@ -310,8 +320,9 @@ class Tiles(TaskNestedView): hillshade = float(hillshade) if hillshade <= 0: hillshade = 1.0 + print(hillshade) except ValueError: - hillshade = 1.0 + raise exceptions.ValidationError("Invalid hillshade value") if tile.shape[0] != 1: raise exceptions.ValidationError("Cannot compute hillshade of non-elevation raster (multiple bands found)") diff --git a/app/fixtures/tiny_drone_image_multispec.tif b/app/fixtures/tiny_drone_image_multispec.tif new file mode 100644 index 00000000..a986ab20 Binary files /dev/null and b/app/fixtures/tiny_drone_image_multispec.tif differ diff --git a/app/static/app/js/classes/Utils.js b/app/static/app/js/classes/Utils.js index 072090eb..b252f88a 100644 --- a/app/static/app/js/classes/Utils.js +++ b/app/static/app/js/classes/Utils.js @@ -81,18 +81,6 @@ export default { } }, - // https://stackoverflow.com/questions/11688692/how-to-create-a-list-of-unique-items-in-javascript - unique: function(arr){ - let u = {}, a = []; - for(let i = 0, l = arr.length; i < l; ++i){ - if(!u.hasOwnProperty(arr[i])) { - a.push(arr[i]); - u[arr[i]] = 1; - } - } - return a; - }, - getCurrentScriptDir: function(){ let scripts= document.getElementsByTagName('script'); let path= scripts[scripts.length-1].src.split('?')[0]; // remove any ?query diff --git a/app/static/app/js/components/tests/LayersControl.test.jsx b/app/static/app/js/components/tests/LayersControl.test.jsx new file mode 100644 index 00000000..b72fc3d9 --- /dev/null +++ b/app/static/app/js/components/tests/LayersControl.test.jsx @@ -0,0 +1,7 @@ +import LayersControl from '../LayersControl'; + +describe('', () => { + it('compiled without exploding', () => { + expect(LayersControl.prototype.onAdd !== undefined).toBe(true); + }) +}); \ No newline at end of file diff --git a/app/static/app/js/components/tests/LayersControlLayer.test.jsx b/app/static/app/js/components/tests/LayersControlLayer.test.jsx new file mode 100644 index 00000000..2c65cc18 --- /dev/null +++ b/app/static/app/js/components/tests/LayersControlLayer.test.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import LayersControlLayer from '../LayersControlLayer'; + +describe('', () => { + it('renders without exploding', () => { + const map = { + hasLayer: () => true + }; + const wrapper = mount(); + expect(wrapper.exists()).toBe(true); + }) +}); \ No newline at end of file diff --git a/app/static/app/js/css/ModelView.scss b/app/static/app/js/css/ModelView.scss index fb99802b..15f1c163 100644 --- a/app/static/app/js/css/ModelView.scss +++ b/app/static/app/js/css/ModelView.scss @@ -73,7 +73,7 @@ } .asset-download-buttons{ .dropdown-menu{ - left: -100%; + left: -150px; } } diff --git a/app/tests/test_api_task.py b/app/tests/test_api_task.py index 05e8798c..5d9269e5 100644 --- a/app/tests/test_api_task.py +++ b/app/tests/test_api_task.py @@ -17,7 +17,8 @@ import worker from django.utils import timezone from app import pending_actions -from app.api.formulas import algos +from app.api.formulas import algos, get_camera_filters_for +from app.cogeo import valid_cogeo from app.models import Project, Task, ImageUpload from app.models.task import task_directory_path, full_task_directory_path, TaskInterruptedException from app.plugins.signals import task_completed, task_removed, task_removing @@ -71,6 +72,8 @@ class TestApiTask(BootTransactionTestCase): # task creation via file upload image1 = open("app/fixtures/tiny_drone_image.jpg", 'rb') image2 = open("app/fixtures/tiny_drone_image_2.jpg", 'rb') + multispec_image = open("app/fixtures/tiny_drone_image_multispec.tif", 'rb') + img1 = Image.open("app/fixtures/tiny_drone_image.jpg") @@ -135,7 +138,7 @@ class TestApiTask(BootTransactionTestCase): testWatch.clear() gcp = open("app/fixtures/gcp.txt", 'r') res = client.post("/api/projects/{}/tasks/".format(project.id), { - 'images': [image1, image2, gcp], + 'images': [image1, image2, multispec_image, gcp], 'name': 'test_task', 'processing_node': pnode.id, 'resize_to': img1.size[0] / 2.0 @@ -145,11 +148,16 @@ class TestApiTask(BootTransactionTestCase): image1.seek(0) image2.seek(0) gcp.seek(0) + multispec_image.seek(0) # Uploaded images should have been resized with Image.open(resized_task.task_path("tiny_drone_image.jpg")) as im: self.assertTrue(im.size[0] == img1.size[0] / 2.0) + # Except the multispectral image + with Image.open(resized_task.task_path("tiny_drone_image_multispec.tif")) as im: + self.assertTrue(im.size[0] == img1.size[0]) + # GCP should have been scaled with open(resized_task.task_path("gcp.txt")) as f: lines = list(map(lambda l: l.strip(), f.readlines())) @@ -337,6 +345,11 @@ class TestApiTask(BootTransactionTestCase): self.assertTrue(res.status_code == status.HTTP_200_OK) self.assertTrue(res.has_header('_stream')) + # The tif files are valid Cloud Optimized GeoTIFF + self.assertTrue(valid_cogeo(task.assets_path(task.ASSETS_MAP["orthophoto.tif"]))) + self.assertTrue(valid_cogeo(task.assets_path(task.ASSETS_MAP["dsm.tif"]))) + self.assertTrue(valid_cogeo(task.assets_path(task.ASSETS_MAP["dtm.tif"]))) + # A textured mesh archive file should exist self.assertTrue(os.path.exists(task.assets_path(task.ASSETS_MAP["textured_model.zip"]["deferred_path"]))) @@ -478,8 +491,48 @@ class TestApiTask(BootTransactionTestCase): self.assertEqual(i.width, 512) self.assertEqual(i.height, 512) - # TODO: Test hillshade + # Cannot access tile 0/0/0 + res = client.get("/api/projects/{}/tasks/{}/orthophoto/tiles/0/0/0.png".format(project.id, task.id)) + self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) + # Can access hillshade, formulas, bands, rescale, color_map + params = [ + ("dsm", "color_map=jet_r&hillshade=3&rescale=150,170", status.HTTP_200_OK), + ("dsm", "color_map=jet_r&hillshade=0&rescale=150,170", status.HTTP_200_OK), + ("dsm", "color_map=invalid&rescale=150,170", status.HTTP_400_BAD_REQUEST), + ("dsm", "color_map=jet_r&rescale=invalid", status.HTTP_400_BAD_REQUEST), + ("dsm", "color_map=jet_r&rescale=150,170&hillshade=invalid", status.HTTP_400_BAD_REQUEST), + + ("dtm", "hillshade=3", status.HTTP_200_OK), + ("dtm", "hillshade=99999999999999999999999999999999999", status.HTTP_200_OK), + ("dtm", "hillshade=-9999999999999999999999999999999999", status.HTTP_200_OK), + ("dtm", "hillshade=0", status.HTTP_200_OK), + + ("orthophoto", "hillshade=3", status.HTTP_400_BAD_REQUEST), + + ("orthophoto", "formula=NDVI&bands=RGN", status.HTTP_200_OK), + ("orthophoto", "formula=VARI&bands=RGN", status.HTTP_400_BAD_REQUEST), + ("orthophoto", "formula=VARI&bands=RGB", status.HTTP_200_OK), + ("orthophoto", "formula=VARI&bands=invalid", status.HTTP_400_BAD_REQUEST), + ("orthophoto", "formula=invalid&bands=RGB", status.HTTP_400_BAD_REQUEST), + + ("orthophoto", "formula=NDVI&bands=RGN&color_map=rdylgn&rescale=-1,1", status.HTTP_200_OK), + ("orthophoto", "formula=NDVI&bands=RGN&color_map=rdylgn&rescale=1,-1", status.HTTP_200_OK), + + ("orthophoto", "formula=NDVI&bands=RGN&color_map=invalid", status.HTTP_400_BAD_REQUEST), + ] + + for k in algos: + a = algos[k] + filters = get_camera_filters_for(a) + self.assertTrue(len(filters) > 0, "%s has filters" % k) + + for f in filters: + params.append(("orthophoto", "formula={}&bands={}&color_map=rdylgn".format(k, f), status.HTTP_200_OK)) + + for tile_type, url, sc in params: + res = client.get("/api/projects/{}/tasks/{}/{}/tiles/17/32042/46185.png?{}".format(project.id, task.id, tile_type, url)) + self.assertEqual(res.status_code, sc) # Another user does not have access to the resources other_client = APIClient() @@ -709,9 +762,10 @@ class TestApiTask(BootTransactionTestCase): self.assertFalse('orthophoto_tiles.zip' in res.data['available_assets']) self.assertTrue('textured_model.zip' in res.data['available_assets']) - image1.close() - image2.close() - gcp.close() + image1.close() + image2.close() + multispec_image.close() + gcp.close() def test_task_auto_processing_node(self): project = Project.objects.get(name="User Test Project") diff --git a/app/tests/test_api_task_import.py b/app/tests/test_api_task_import.py index 7cf287ae..fb557501 100644 --- a/app/tests/test_api_task_import.py +++ b/app/tests/test_api_task_import.py @@ -9,6 +9,7 @@ from rest_framework import status from rest_framework.test import APIClient import worker +from app.cogeo import valid_cogeo from app.models import Project from app.models import Task from app.tests.classes import BootTransactionTestCase @@ -115,6 +116,10 @@ class TestApiTask(BootTransactionTestCase): res = client.get("/api/projects/{}/tasks/{}/assets/odm_orthophoto/odm_orthophoto.tif".format(project.id, file_import_task.id)) self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertTrue(valid_cogeo(file_import_task.assets_path(task.ASSETS_MAP["orthophoto.tif"]))) + self.assertTrue(valid_cogeo(file_import_task.assets_path(task.ASSETS_MAP["dsm.tif"]))) + self.assertTrue(valid_cogeo(file_import_task.assets_path(task.ASSETS_MAP["dtm.tif"]))) + # Set task public so we can download from it without auth file_import_task.public = True file_import_task.save()