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.