diff --git a/app/api/tasks.py b/app/api/tasks.py index 23ac7c64..602dc004 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -35,7 +35,7 @@ class TaskSerializer(serializers.ModelSerializer): class Meta: model = models.Task - exclude = ('processing_lock', 'console_output', 'orthophoto_extent', ) + exclude = ('processing_lock', 'console_output', 'orthophoto_extent', 'dsm_extent', 'dtm_extent', ) read_only_fields = ('processing_time', 'status', 'last_error', 'created_at', 'pending_action', 'available_assets', ) class TaskViewSet(viewsets.ViewSet): @@ -44,7 +44,7 @@ class TaskViewSet(viewsets.ViewSet): A task represents a set of images and other input to be sent to a processing node. Once a processing node completes processing, results are stored in the task. """ - queryset = models.Task.objects.all().defer('orthophoto_extent', 'console_output') + queryset = models.Task.objects.all().defer('orthophoto_extent', 'dsm_extent', 'dtm_extent', 'console_output', ) # We don't use object level permissions on tasks, relying on # project's object permissions instead (but standard model permissions still apply) @@ -170,7 +170,7 @@ class TaskViewSet(viewsets.ViewSet): class TaskNestedView(APIView): - queryset = models.Task.objects.all().defer('orthophoto_extent', 'console_output') + queryset = models.Task.objects.all().defer('orthophoto_extent', 'dtm_extent', 'dsm_extent', 'console_output', ) def get_and_check_task(self, request, pk, project_pk, annotate={}): get_and_check_project(request, project_pk) @@ -182,12 +182,12 @@ class TaskNestedView(APIView): class TaskTiles(TaskNestedView): - def get(self, request, pk=None, project_pk=None, z="", x="", y=""): + def get(self, request, pk=None, project_pk=None, tile_type="", z="", x="", y=""): """ - Get an orthophoto tile + Get a tile image """ task = self.get_and_check_task(request, pk, project_pk) - tile_path = task.get_tile_path(z, x, y) + 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") @@ -196,18 +196,29 @@ class TaskTiles(TaskNestedView): class TaskTilesJson(TaskNestedView): - def get(self, request, pk=None, project_pk=None): + def get(self, request, pk=None, project_pk=None, tile_type=""): """ - Get tile.json for this tasks's orthophoto + Get tile.json for this tasks's asset type """ task = self.get_and_check_task(request, pk, project_pk) - if task.orthophoto_extent is None: - raise exceptions.ValidationError("An orthophoto has not been processed for this task. Tiles are not available.") + 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)) json = get_tile_json(task.name, [ - '/api/projects/{}/tasks/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id) - ], task.orthophoto_extent.extent) + '/api/projects/{}/tasks/{}/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id, tile_type) + ], extent.extent) return Response(json) diff --git a/app/api/urls.py b/app/api/urls.py index 8f1b26a4..30b55edf 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -18,8 +18,9 @@ urlpatterns = [ url(r'^', include(router.urls)), url(r'^', include(tasks_router.urls)), - url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/tiles/(?P[\d]+)/(?P[\d]+)/(?P[\d]+)\.png$', TaskTiles.as_view()), - url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/tiles\.json$', TaskTilesJson.as_view()), + url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/tiles/(?Porthophoto|dsm|dtm)/(?P[\d]+)/(?P[\d]+)/(?P[\d]+)\.png$', TaskTiles.as_view()), + url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/(?Porthophoto|dsm|dtm)/tiles\.json$', TaskTilesJson.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/migrations/0007_auto_20170712_1319.py b/app/migrations/0007_auto_20170712_1319.py new file mode 100644 index 00000000..90f0512a --- /dev/null +++ b/app/migrations/0007_auto_20170712_1319.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.1 on 2017-07-12 17:19 +from __future__ import unicode_literals + +import django.contrib.gis.db.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0006_task_available_assets'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='dsm_extent', + field=django.contrib.gis.db.models.fields.GeometryField(blank=True, help_text='Extent of the DSM created by OpenDroneMap', null=True, srid=4326), + ), + migrations.AddField( + model_name='task', + name='dtm_extent', + field=django.contrib.gis.db.models.fields.GeometryField(blank=True, help_text='Extent of the DTM created by OpenDroneMap', null=True, srid=4326), + ), + ] diff --git a/app/models.py b/app/models.py index ebf9af00..934a4e0e 100644 --- a/app/models.py +++ b/app/models.py @@ -12,6 +12,7 @@ from django.contrib.postgres import fields from django.core.exceptions import ValidationError from django.db import models from django.db import transaction +from django.db.models import Q from django.db.models import signals from django.dispatch import receiver from django.utils import timezone @@ -71,11 +72,11 @@ class Project(models.Model): def tasks(self): return self.task_set.only('id') - def get_tile_json_data(self): - return [task.get_tile_json_data() for task in self.task_set.filter( - status=status_codes.COMPLETED, - orthophoto_extent__isnull=False - ).only('id', 'project_id')] + def get_tiles_json_data(self): + return [task.get_tiles_json_data() for task in self.task_set.filter( + status=status_codes.COMPLETED + ).filter(Q(orthophoto_extent__isnull=False) | Q(dsm_extent__isnull=False) | Q(dtm_extent__isnull=False)) + .only('id', 'project_id')] class Meta: permissions = ( @@ -130,7 +131,9 @@ class Task(models.Model): 'textured_model.zip': { 'deferred_path': 'textured_model.zip', 'deferred_compress_dir': 'odm_texturing' - } + }, + 'dtm.tif': os.path.join('odm_dem', 'dtm.tif'), + 'dsm.tif': os.path.join('odm_dem', 'dsm.tif'), } STATUS_CODES = ( @@ -138,7 +141,7 @@ class Task(models.Model): (status_codes.RUNNING, 'RUNNING'), (status_codes.FAILED, 'FAILED'), (status_codes.COMPLETED, 'COMPLETED'), - (status_codes.CANCELED, 'CANCELED') + (status_codes.CANCELED, 'CANCELED'), ) PENDING_ACTIONS = ( @@ -162,6 +165,8 @@ class Task(models.Model): ground_control_points = models.FileField(null=True, blank=True, upload_to=gcp_directory_path, help_text="Optional Ground Control Points file to use for processing") orthophoto_extent = GeometryField(null=True, blank=True, srid=4326, help_text="Extent of the orthophoto created by OpenDroneMap") + dsm_extent = GeometryField(null=True, blank=True, srid=4326, help_text="Extent of the DSM created by OpenDroneMap") + dtm_extent = GeometryField(null=True, blank=True, srid=4326, help_text="Extent of the DTM created by OpenDroneMap") # mission created_at = models.DateTimeField(default=timezone.now, help_text="Creation date") @@ -438,17 +443,26 @@ class Task(models.Model): logger.info("Extracted all.zip for {}".format(self)) - # Populate orthophoto_extent field - orthophoto_path = os.path.realpath(self.assets_path("odm_orthophoto", "odm_orthophoto.tif")) - if os.path.exists(orthophoto_path): - # Read extent and SRID - orthophoto = GDALRaster(orthophoto_path) - extent = OGRGeometry.from_bbox(orthophoto.extent) + # Populate *_extent fields + extent_fields = [ + (os.path.realpath(self.assets_path("odm_orthophoto", "odm_orthophoto.tif")), + self.orthophoto_extent), + (os.path.realpath(self.assets_path("odm_dsm", "dsm.tif")), + self.dsm_extent), + (os.path.realpath(self.assets_path("odm_dtm", "dtm.tif")), + self.dtm_extent), + ] - # It will be implicitly transformed into the SRID of the model’s field - self.orthophoto_extent = GEOSGeometry(extent.wkt, srid=orthophoto.srid) + for raster_path, field in extent_fields: + if os.path.exists(raster_path): + # Read extent and SRID + raster = GDALRaster(raster_path) + extent = OGRGeometry.from_bbox(raster.extent) - logger.info("Populated orthophoto_extent for {}".format(self)) + # It will be implicitly transformed into the SRID of the model’s field + field = GEOSGeometry(extent.wkt, srid=raster.srid) + + logger.info("Populated extent field with {} for {}".format(raster_path, self)) self.update_available_assets_field() self.save() @@ -467,15 +481,20 @@ class Task(models.Model): logger.warning("{} timed out with error: {}. We'll try reprocessing at the next tick.".format(self, str(e))) - def get_tile_path(self, z, x, y): - return self.assets_path("orthophoto_tiles", z, x, "{}.png".format(y)) + def get_tile_path(self, tile_type, z, x, y): + return self.assets_path("{}_tiles".format(tile_type), z, x, "{}.png".format(y)) - def get_tile_json_url(self): - return "/api/projects/{}/tasks/{}/tiles.json".format(self.project.id, self.id) + def get_tile_json_url(self, tile_type): + return "/api/projects/{}/tasks/{}/{}/tiles.json".format(self.project.id, self.id, tile_type) + + def get_tiles_json_data(self): + types = [] + if 'orthophoto.tif' in self.available_assets: types.append('orthophoto') + if 'dsm.tif' in self.available_assets: types.append('dsm') + if 'dtm.tif' in self.available_assets: types.append('dtm') - def get_tile_json_data(self): return { - 'url': self.get_tile_json_url(), + 'tiles': [{'url': self.get_tile_json_url(t), 'type': t} for t in types], 'meta': { 'task': self.id, 'project': self.project.id diff --git a/app/static/app/js/MapView.jsx b/app/static/app/js/MapView.jsx index 2f8176bc..4004d6bd 100644 --- a/app/static/app/js/MapView.jsx +++ b/app/static/app/js/MapView.jsx @@ -5,20 +5,27 @@ import $ from 'jquery'; class MapView extends React.Component { static defaultProps = { - tiles: [] + tiles: [], + selectedMapType: 'orthophoto', + title: "" }; static propTypes = { - tiles: React.PropTypes.array.isRequired // tiles.json list + tiles: React.PropTypes.array.isRequired, // list of dictionaries where each dict is a {mapType: 'orthophoto', url: }, + selectedMapType: React.PropTypes.oneOf(['orthophoto', 'dsm', 'dtm']), + title: React.PropTypes.string, }; constructor(props){ super(props); this.state = { - opacity: 100 + opacity: 100, + mapType: props.mapType }; + console.log(props); + this.updateOpacity = this.updateOpacity.bind(this); } @@ -30,8 +37,31 @@ class MapView extends React.Component { render(){ const { opacity } = this.state; + const mapTypeButtons = [ + { + label: "Orthophoto", + key: "orthophoto" + }, + { + label: "Surface Model", + key: "dsm" + }, + { + label: "Terrain Model", + key: "dtm" + } + ]; return (
+
+ + +
+ + {this.props.title ? +

{this.props.title}

+ : ""} +
Opacity: diff --git a/app/static/app/js/classes/AssetDownloads.js b/app/static/app/js/classes/AssetDownloads.js index bea0187b..eb6fe29e 100644 --- a/app/static/app/js/classes/AssetDownloads.js +++ b/app/static/app/js/classes/AssetDownloads.js @@ -33,6 +33,9 @@ const api = { return [ new AssetDownload("Orthophoto (GeoTIFF)","orthophoto.tif","fa fa-map-o"), new AssetDownload("Orthophoto (PNG)","orthophoto.png","fa fa-picture-o"), + new AssetDownload("Terrain Model (GeoTIFF)","dtm.tif","fa fa-area-chart"), + new AssetDownload("Surface Model (GeoTIFF)","dsm.tif","fa fa-area-chart"), + new AssetDownload("Point Cloud (LAS)","georeferenced_model.las","fa fa-cube"), new AssetDownload("Point Cloud (LAS)","georeferenced_model.las","fa fa-cube"), new AssetDownload("Point Cloud (PLY)","georeferenced_model.ply","fa fa-cube"), new AssetDownload("Point Cloud (CSV)","georeferenced_model.csv","fa fa-cube"), diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index feafadd3..818335c0 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -21,7 +21,8 @@ class Map extends React.Component { maxzoom: 18, minzoom: 0, showBackground: false, - opacity: 100 + opacity: 100, + mapType: "orthophoto" }; static propTypes = { @@ -29,7 +30,8 @@ class Map extends React.Component { minzoom: React.PropTypes.number, showBackground: React.PropTypes.bool, tiles: React.PropTypes.array.isRequired, - opacity: React.PropTypes.number + opacity: React.PropTypes.number, + mapType: React.PropTypes.oneOf(['orthophoto', 'dsm', 'dtm']) }; constructor(props) { diff --git a/app/static/app/js/components/TaskListItem.jsx b/app/static/app/js/components/TaskListItem.jsx index 41171ef2..8840f0d0 100644 --- a/app/static/app/js/components/TaskListItem.jsx +++ b/app/static/app/js/components/TaskListItem.jsx @@ -271,7 +271,7 @@ class TaskListItem extends React.Component { if (task.status === statusCodes.COMPLETED){ if (task.available_assets.indexOf("orthophoto.tif") !== -1){ - addActionButton(" View Orthophoto", "btn-primary", "fa fa-globe", () => { + addActionButton(" View Map", "btn-primary", "fa fa-globe", () => { location.href = `/map/project/${task.project}/task/${task.id}/`; }); }else{ diff --git a/app/static/app/js/css/MapView.scss b/app/static/app/js/css/MapView.scss index 58efc1fe..09d11fe5 100644 --- a/app/static/app/js/css/MapView.scss +++ b/app/static/app/js/css/MapView.scss @@ -18,11 +18,15 @@ text-align: center; width: 220px; position: absolute; - bottom: 12px; + bottom: -32px; left: 50%; margin-left: -100px; z-index: 400; padding-bottom: 6px; background-color: white; } + + .map-type-selector{ + float: right; + } } \ No newline at end of file diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers.js b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers.js index f7e91dc5..2cb11873 100644 --- a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers.js +++ b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers.js @@ -167,7 +167,7 @@ L.Control.AutoLayers = L.Control.extend({ 'leaflet-control-layers-tab', form); this._overlaysLayersTitle = L.DomUtil.create('div', 'leaflet-control-autolayers-title', overlaysLayersDiv); - this._overlaysLayersTitle.innerHTML = 'Orthophotos'; + this._overlaysLayersTitle.innerHTML = 'Tasks'; var overlaysLayersBox = this._overlaysLayersBox = L.DomUtil.create('div', 'map-filter', overlaysLayersDiv); var overlaysLayersFilter = this._overlaysLayersFilter = L.DomUtil.create('input', diff --git a/app/templates/app/map.html b/app/templates/app/map.html index 18b88fe2..acf75adc 100644 --- a/app/templates/app/map.html +++ b/app/templates/app/map.html @@ -5,8 +5,6 @@ {% load render_bundle from webpack_loader %} {% render_bundle 'MapView' attrs='async' %} -

{{title}}

-