diff --git a/app/static/app/js/MapView.jsx b/app/static/app/js/MapView.jsx index 74258e42..877a7682 100644 --- a/app/static/app/js/MapView.jsx +++ b/app/static/app/js/MapView.jsx @@ -31,8 +31,8 @@ class MapView extends React.Component { } getTilesByMapType(type){ - // Go through the list of tiles and return - // only those that match a particular type + // Go through the list of map items and return + // only those that match a particular type (in tile format) const tiles = []; this.props.mapItems.forEach(mapItem => { @@ -64,7 +64,7 @@ class MapView extends React.Component { render(){ const { opacity } = this.state; - const mapTypeButtons = [ + let mapTypeButtons = [ { label: "Orthophoto", type: "orthophoto" @@ -77,7 +77,10 @@ class MapView extends React.Component { label: "Terrain Model", type: "dtm" } - ]; + ].filter(mapType => this.getTilesByMapType(mapType.type).length > 0 ); + + // If we have only one button, hide it... + if (mapTypeButtons.length === 1) mapTypeButtons = []; return (
diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index f0f49ad8..8835f7d9 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -45,18 +45,31 @@ class Map extends React.Component { this.imageryLayers = []; this.basemaps = {}; this.mapBounds = null; + this.autolayers = null; this.loadImageryLayers = this.loadImageryLayers.bind(this); } - loadImageryLayers(){ + loadImageryLayers(forceAddLayers = false){ const { tiles } = this.props, - assets = AssetDownloads.excludeSeparators(); + assets = AssetDownloads.excludeSeparators(), + layerId = layer => { + const meta = layer[Symbol.for("meta")]; + return meta.project + "_" + meta.task; + }; // Remove all previous imagery layers - this.imageryLayers.forEach(layer => layer.remove()); + // and keep track of which ones were selected + const prevSelectedLayers = []; + + this.imageryLayers.forEach(layer => { + this.autolayers.removeLayer(layer); + if (this.map.hasLayer(layer)) prevSelectedLayers.push(layerId(layer)); + layer.remove(); + }); this.imageryLayers = []; + // Request new tiles return new Promise((resolve, reject) => { this.tileJsonRequests = []; @@ -74,12 +87,16 @@ class Map extends React.Component { maxZoom: info.maxzoom, tms: info.scheme === 'tms', opacity: this.props.opacity / 100 - }).addTo(this.map); - + }); + // Associate metadata with this layer meta.name = info.name; layer[Symbol.for("meta")] = meta; + if (forceAddLayers || prevSelectedLayers.indexOf(layerId(layer)) !== -1){ + layer.addTo(this.map); + } + // Show 3D switch button only if we have a single orthophoto const task = { id: meta.task, @@ -118,6 +135,9 @@ class Map extends React.Component { mapBounds.extend(bounds); this.mapBounds = mapBounds; + // Add layer to layers control + this.autolayers.addOverlay(layer, info.name); + done(); }) .fail((_, __, err) => done(err)) @@ -138,10 +158,17 @@ class Map extends React.Component { this.map = Leaflet.map(this.container, { scrollWheelZoom: true, - measureControl: true, positionControl: true }); + const measureControl = Leaflet.control.measure({ + primaryLengthUnit: 'meters', + secondaryLengthUnit: 'feet', + primaryAreaUnit: 'sqmeters', + secondaryAreaUnit: 'acres' + }); + measureControl.addTo(this.map); + if (showBackground) { this.basemaps = { "Google Maps Hybrid": L.tileLayer('//{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', { @@ -166,6 +193,12 @@ class Map extends React.Component { }; } + this.autolayers = Leaflet.control.autolayers({ + overlays: {}, + selectedOverlays: [], + baseLayers: this.basemaps + }).addTo(this.map); + this.map.fitWorld(); Leaflet.control.scale({ @@ -173,22 +206,9 @@ class Map extends React.Component { }).addTo(this.map); this.map.attributionControl.setPrefix(""); - this.loadImageryLayers().then(() => { + this.loadImageryLayers(true).then(() => { this.map.fitBounds(this.mapBounds); - // Add basemaps / layers control - let overlays = {}; - this.imageryLayers.forEach(layer => { - const meta = layer[Symbol.for("meta")]; - overlays[meta.name] = layer; - }); - - Leaflet.control.autolayers({ - overlays: overlays, - selectedOverlays: [], - baseLayers: this.basemaps - }).addTo(this.map); - this.map.on('click', e => { // Find first tile layer at the selected coordinates for (let layer of this.imageryLayers){ @@ -199,7 +219,6 @@ class Map extends React.Component { } }); }); - } componentDidUpdate(prevProps) { @@ -208,7 +227,9 @@ class Map extends React.Component { }); if (prevProps.tiles !== this.props.tiles){ - this.loadImageryLayers(); + this.loadImageryLayers().then(() => { + // console.log("GOT: ", this.autolayers, this.autolayers.selectedOverlays); + }); } } diff --git a/app/tests/test_api_task.py b/app/tests/test_api_task.py index c1336adc..5616b0e6 100644 --- a/app/tests/test_api_task.py +++ b/app/tests/test_api_task.py @@ -156,20 +156,22 @@ class TestApiTask(BootTransactionTestCase): self.assertTrue(task.processing_node is None) # tiles.json should not be accessible at this point - res = client.get("/api/projects/{}/tasks/{}/tiles.json".format(project.id, task.id)) - self.assertTrue(res.status_code == status.HTTP_400_BAD_REQUEST) + tile_types = ['orthophoto', 'dsm', 'dtm'] + for tile_type in tile_types: + res = client.get("/api/projects/{}/tasks/{}/{}/tiles.json".format(project.id, task.id, tile_type)) + self.assertTrue(res.status_code == status.HTTP_400_BAD_REQUEST) # Neither should an individual tile # Z/X/Y coords are choosen based on node-odm test dataset for orthophoto_tiles/ - res = client.get("/api/projects/{}/tasks/{}/tiles/16/16020/42443.png".format(project.id, task.id)) + res = client.get("/api/projects/{}/tasks/{}/orthophoto/tiles/16/16020/42443.png".format(project.id, task.id)) self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND) # Cannot access a tiles.json we have no access to - res = client.get("/api/projects/{}/tasks/{}/tiles.json".format(other_project.id, other_task.id)) + res = client.get("/api/projects/{}/tasks/{}/orthophoto/tiles.json".format(other_project.id, other_task.id)) self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND) # Cannot access an individual tile we have no access to - res = client.get("/api/projects/{}/tasks/{}/tiles/16/16020/42443.png".format(other_project.id, other_task.id)) + res = client.get("/api/projects/{}/tasks/{}/orthophoto/tiles/16/16020/42443.png".format(other_project.id, other_task.id)) self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND) # Cannot download assets (they don't exist yet) @@ -226,8 +228,9 @@ class TestApiTask(BootTransactionTestCase): self.assertTrue(res.status_code == status.HTTP_200_OK) # Can access tiles.json - res = client.get("/api/projects/{}/tasks/{}/tiles.json".format(project.id, task.id)) - self.assertTrue(res.status_code == status.HTTP_200_OK) + for tile_type in tile_types: + res = client.get("/api/projects/{}/tasks/{}/{}/tiles.json".format(project.id, task.id, tile_type)) + self.assertTrue(res.status_code == status.HTTP_200_OK) # Bounds are what we expect them to be # (4 coords in lat/lon) @@ -236,8 +239,9 @@ class TestApiTask(BootTransactionTestCase): self.assertTrue(round(tiles['bounds'][0], 7) == -91.9945132) # Can access individual tiles - res = client.get("/api/projects/{}/tasks/{}/tiles/16/16020/42443.png".format(project.id, task.id)) - self.assertTrue(res.status_code == status.HTTP_200_OK) + for tile_type in tile_types: + res = client.get("/api/projects/{}/tasks/{}/{}/tiles/16/16020/42443.png".format(project.id, task.id, tile_type)) + self.assertTrue(res.status_code == status.HTTP_200_OK) # Restart a task testWatch.clear() @@ -379,6 +383,20 @@ class TestApiTask(BootTransactionTestCase): # orthophoto_extent should be none self.assertTrue(task.orthophoto_extent is None) + # but other extents should be populated + self.assertTrue(task.dsm_extent is not None) + self.assertTrue(task.dtm_extent is not None) + self.assertTrue(os.path.exists(task.assets_path("dsm_tiles"))) + self.assertTrue(os.path.exists(task.assets_path("dtm_tiles"))) + + # Can access only tiles of available assets + res = client.get("/api/projects/{}/tasks/{}/dsm/tiles.json".format(project.id, task.id)) + self.assertTrue(res.status_code == status.HTTP_200_OK) + res = client.get("/api/projects/{}/tasks/{}/dtm/tiles.json".format(project.id, task.id)) + self.assertTrue(res.status_code == status.HTTP_200_OK) + res = client.get("/api/projects/{}/tasks/{}/orthophoto/tiles.json".format(project.id, task.id)) + self.assertTrue(res.status_code == status.HTTP_400_BAD_REQUEST) + # Available assets should be missing orthophoto.tif type # but others such as textured_model.zip should be available res = client.get("/api/projects/{}/tasks/{}/".format(project.id, task.id)) diff --git a/nodeodm/external/node-OpenDroneMap b/nodeodm/external/node-OpenDroneMap index bfc90c9c..3586a31f 160000 --- a/nodeodm/external/node-OpenDroneMap +++ b/nodeodm/external/node-OpenDroneMap @@ -1 +1 @@ -Subproject commit bfc90c9cec21a06b88ed92f202a0a79901b8962d +Subproject commit 3586a31f9071db5931b52c580bbf009f25e3b5ec diff --git a/slate/source/includes/reference/_task.md b/slate/source/includes/reference/_task.md index 0a4b496f..2591a3fa 100644 --- a/slate/source/includes/reference/_task.md +++ b/slate/source/includes/reference/_task.md @@ -180,14 +180,26 @@ If a [Task](#task) has been canceled or has failed processing, or has completed ### Orthophoto TMS layer -`GET /api/projects/{project_id}/tasks/{task_id}/tiles.json` +`GET /api/projects/{project_id}/tasks/{task_id}/orthophoto/tiles.json` -`GET /api/projects/{project_id}/tasks/{task_id}/tiles/{Z}/{X}/{Y}.png` +`GET /api/projects/{project_id}/tasks/{task_id}/orthophoto/tiles/{Z}/{X}/{Y}.png` After a task has been successfully processed, a TMS layer is made available for inclusion in programs such as [Leaflet](http://leafletjs.com/) or [Cesium](http://cesiumjs.org). +### Surface Model TMS layer + +`GET /api/projects/{project_id}/tasks/{task_id}/dsm/tiles.json` + +`GET /api/projects/{project_id}/tasks/{task_id}/dsm/tiles/{Z}/{X}/{Y}.png` + +### Terrain Model TMS layer + +`GET /api/projects/{project_id}/tasks/{task_id}/dtm/tiles.json` + +`GET /api/projects/{project_id}/tasks/{task_id}/dtm/tiles/{Z}/{X}/{Y}.png` + ### Pending Actions In some circumstances, a [Task](#task) can have a pending action that requires some amount of time to be performed.