Started adding support to display DSM, DTM map tiles

pull/237/head
Piero Toffanin 2017-07-12 13:35:28 -04:00
rodzic 3f7e56177b
commit 186f4bac5e
13 zmienionych plików z 149 dodań i 52 usunięć

Wyświetl plik

@ -35,7 +35,7 @@ class TaskSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Task 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', ) read_only_fields = ('processing_time', 'status', 'last_error', 'created_at', 'pending_action', 'available_assets', )
class TaskViewSet(viewsets.ViewSet): 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. 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. 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 # We don't use object level permissions on tasks, relying on
# project's object permissions instead (but standard model permissions still apply) # project's object permissions instead (but standard model permissions still apply)
@ -170,7 +170,7 @@ class TaskViewSet(viewsets.ViewSet):
class TaskNestedView(APIView): 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={}): def get_and_check_task(self, request, pk, project_pk, annotate={}):
get_and_check_project(request, project_pk) get_and_check_project(request, project_pk)
@ -182,12 +182,12 @@ class TaskNestedView(APIView):
class TaskTiles(TaskNestedView): 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) 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): if os.path.isfile(tile_path):
tile = open(tile_path, "rb") tile = open(tile_path, "rb")
return HttpResponse(FileWrapper(tile), content_type="image/png") return HttpResponse(FileWrapper(tile), content_type="image/png")
@ -196,18 +196,29 @@ class TaskTiles(TaskNestedView):
class TaskTilesJson(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) task = self.get_and_check_task(request, pk, project_pk)
if task.orthophoto_extent is None: extent_map = {
raise exceptions.ValidationError("An orthophoto has not been processed for this task. Tiles are not available.") '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, [ json = get_tile_json(task.name, [
'/api/projects/{}/tasks/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id) '/api/projects/{}/tasks/{}/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id, tile_type)
], task.orthophoto_extent.extent) ], extent.extent)
return Response(json) return Response(json)

Wyświetl plik

@ -18,8 +18,9 @@ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^', include(tasks_router.urls)), url(r'^', include(tasks_router.urls)),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/tiles/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)\.png$', TaskTiles.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/tiles/(?P<tile_type>orthophoto|dsm|dtm)/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)\.png$', TaskTiles.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/tiles\.json$', TaskTilesJson.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles\.json$', TaskTilesJson.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()),

Wyświetl plik

@ -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),
),
]

Wyświetl plik

@ -12,6 +12,7 @@ from django.contrib.postgres import fields
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db import transaction from django.db import transaction
from django.db.models import Q
from django.db.models import signals from django.db.models import signals
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
@ -71,11 +72,11 @@ class Project(models.Model):
def tasks(self): def tasks(self):
return self.task_set.only('id') return self.task_set.only('id')
def get_tile_json_data(self): def get_tiles_json_data(self):
return [task.get_tile_json_data() for task in self.task_set.filter( return [task.get_tiles_json_data() for task in self.task_set.filter(
status=status_codes.COMPLETED, status=status_codes.COMPLETED
orthophoto_extent__isnull=False ).filter(Q(orthophoto_extent__isnull=False) | Q(dsm_extent__isnull=False) | Q(dtm_extent__isnull=False))
).only('id', 'project_id')] .only('id', 'project_id')]
class Meta: class Meta:
permissions = ( permissions = (
@ -130,7 +131,9 @@ class Task(models.Model):
'textured_model.zip': { 'textured_model.zip': {
'deferred_path': 'textured_model.zip', 'deferred_path': 'textured_model.zip',
'deferred_compress_dir': 'odm_texturing' '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 = ( STATUS_CODES = (
@ -138,7 +141,7 @@ class Task(models.Model):
(status_codes.RUNNING, 'RUNNING'), (status_codes.RUNNING, 'RUNNING'),
(status_codes.FAILED, 'FAILED'), (status_codes.FAILED, 'FAILED'),
(status_codes.COMPLETED, 'COMPLETED'), (status_codes.COMPLETED, 'COMPLETED'),
(status_codes.CANCELED, 'CANCELED') (status_codes.CANCELED, 'CANCELED'),
) )
PENDING_ACTIONS = ( 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") 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") 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 # mission
created_at = models.DateTimeField(default=timezone.now, help_text="Creation date") 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)) logger.info("Extracted all.zip for {}".format(self))
# Populate orthophoto_extent field # Populate *_extent fields
orthophoto_path = os.path.realpath(self.assets_path("odm_orthophoto", "odm_orthophoto.tif")) extent_fields = [
if os.path.exists(orthophoto_path): (os.path.realpath(self.assets_path("odm_orthophoto", "odm_orthophoto.tif")),
# Read extent and SRID self.orthophoto_extent),
orthophoto = GDALRaster(orthophoto_path) (os.path.realpath(self.assets_path("odm_dsm", "dsm.tif")),
extent = OGRGeometry.from_bbox(orthophoto.extent) 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 models field for raster_path, field in extent_fields:
self.orthophoto_extent = GEOSGeometry(extent.wkt, srid=orthophoto.srid) 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 models 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.update_available_assets_field()
self.save() 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))) 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): def get_tile_path(self, tile_type, z, x, y):
return self.assets_path("orthophoto_tiles", z, x, "{}.png".format(y)) return self.assets_path("{}_tiles".format(tile_type), z, x, "{}.png".format(y))
def get_tile_json_url(self): def get_tile_json_url(self, tile_type):
return "/api/projects/{}/tasks/{}/tiles.json".format(self.project.id, self.id) 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 { return {
'url': self.get_tile_json_url(), 'tiles': [{'url': self.get_tile_json_url(t), 'type': t} for t in types],
'meta': { 'meta': {
'task': self.id, 'task': self.id,
'project': self.project.id 'project': self.project.id

Wyświetl plik

@ -5,20 +5,27 @@ import $ from 'jquery';
class MapView extends React.Component { class MapView extends React.Component {
static defaultProps = { static defaultProps = {
tiles: [] tiles: [],
selectedMapType: 'orthophoto',
title: ""
}; };
static propTypes = { 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: <tiles.json>},
selectedMapType: React.PropTypes.oneOf(['orthophoto', 'dsm', 'dtm']),
title: React.PropTypes.string,
}; };
constructor(props){ constructor(props){
super(props); super(props);
this.state = { this.state = {
opacity: 100 opacity: 100,
mapType: props.mapType
}; };
console.log(props);
this.updateOpacity = this.updateOpacity.bind(this); this.updateOpacity = this.updateOpacity.bind(this);
} }
@ -30,8 +37,31 @@ class MapView extends React.Component {
render(){ render(){
const { opacity } = this.state; const { opacity } = this.state;
const mapTypeButtons = [
{
label: "Orthophoto",
key: "orthophoto"
},
{
label: "Surface Model",
key: "dsm"
},
{
label: "Terrain Model",
key: "dtm"
}
];
return (<div className="map-view"> return (<div className="map-view">
<div className="map-type-selector btn-group" role="group">
<button className="btn btn-sm btn-primary active">Preview</button>
<button className="btn btn-sm btn-secondary">Source Code</button>
</div>
{this.props.title ?
<h3><i className="fa fa-globe"></i> {this.props.title}</h3>
: ""}
<Map tiles={this.props.tiles} showBackground={true} opacity={opacity}/> <Map tiles={this.props.tiles} showBackground={true} opacity={opacity}/>
<div className="opacity-slider"> <div className="opacity-slider">
Opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} /> Opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} />

Wyświetl plik

@ -33,6 +33,9 @@ const api = {
return [ return [
new AssetDownload("Orthophoto (GeoTIFF)","orthophoto.tif","fa fa-map-o"), new AssetDownload("Orthophoto (GeoTIFF)","orthophoto.tif","fa fa-map-o"),
new AssetDownload("Orthophoto (PNG)","orthophoto.png","fa fa-picture-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 (LAS)","georeferenced_model.las","fa fa-cube"),
new AssetDownload("Point Cloud (PLY)","georeferenced_model.ply","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"), new AssetDownload("Point Cloud (CSV)","georeferenced_model.csv","fa fa-cube"),

Wyświetl plik

@ -21,7 +21,8 @@ class Map extends React.Component {
maxzoom: 18, maxzoom: 18,
minzoom: 0, minzoom: 0,
showBackground: false, showBackground: false,
opacity: 100 opacity: 100,
mapType: "orthophoto"
}; };
static propTypes = { static propTypes = {
@ -29,7 +30,8 @@ class Map extends React.Component {
minzoom: React.PropTypes.number, minzoom: React.PropTypes.number,
showBackground: React.PropTypes.bool, showBackground: React.PropTypes.bool,
tiles: React.PropTypes.array.isRequired, tiles: React.PropTypes.array.isRequired,
opacity: React.PropTypes.number opacity: React.PropTypes.number,
mapType: React.PropTypes.oneOf(['orthophoto', 'dsm', 'dtm'])
}; };
constructor(props) { constructor(props) {

Wyświetl plik

@ -271,7 +271,7 @@ class TaskListItem extends React.Component {
if (task.status === statusCodes.COMPLETED){ if (task.status === statusCodes.COMPLETED){
if (task.available_assets.indexOf("orthophoto.tif") !== -1){ 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}/`; location.href = `/map/project/${task.project}/task/${task.id}/`;
}); });
}else{ }else{

Wyświetl plik

@ -18,11 +18,15 @@
text-align: center; text-align: center;
width: 220px; width: 220px;
position: absolute; position: absolute;
bottom: 12px; bottom: -32px;
left: 50%; left: 50%;
margin-left: -100px; margin-left: -100px;
z-index: 400; z-index: 400;
padding-bottom: 6px; padding-bottom: 6px;
background-color: white; background-color: white;
} }
.map-type-selector{
float: right;
}
} }

Wyświetl plik

@ -167,7 +167,7 @@ L.Control.AutoLayers = L.Control.extend({
'leaflet-control-layers-tab', form); 'leaflet-control-layers-tab', form);
this._overlaysLayersTitle = L.DomUtil.create('div', 'leaflet-control-autolayers-title', this._overlaysLayersTitle = L.DomUtil.create('div', 'leaflet-control-autolayers-title',
overlaysLayersDiv); overlaysLayersDiv);
this._overlaysLayersTitle.innerHTML = 'Orthophotos'; this._overlaysLayersTitle.innerHTML = 'Tasks';
var overlaysLayersBox = this._overlaysLayersBox = L.DomUtil.create('div', 'map-filter', var overlaysLayersBox = this._overlaysLayersBox = L.DomUtil.create('div', 'map-filter',
overlaysLayersDiv); overlaysLayersDiv);
var overlaysLayersFilter = this._overlaysLayersFilter = L.DomUtil.create('input', var overlaysLayersFilter = this._overlaysLayersFilter = L.DomUtil.create('input',

Wyświetl plik

@ -5,8 +5,6 @@
{% load render_bundle from webpack_loader %} {% load render_bundle from webpack_loader %}
{% render_bundle 'MapView' attrs='async' %} {% render_bundle 'MapView' attrs='async' %}
<h3><i class="fa fa-globe"></i> {{title}}</h3>
<div data-mapview <div data-mapview
{% for key, value in params %} {% for key, value in params %}
data-{{key}}="{{value}}" data-{{key}}="{{value}}"

Wyświetl plik

@ -49,17 +49,20 @@ def map(request, project_pk=None, task_pk=None):
raise Http404() raise Http404()
if task_pk is not None: if task_pk is not None:
task = get_object_or_404(Task.objects.defer('orthophoto_extent'), pk=task_pk, project=project) task = get_object_or_404(Task.objects.defer('orthophoto_extent', 'dsm_extent', 'dtm_extent'), pk=task_pk, project=project)
title = task.name title = task.name
tiles = [task.get_tile_json_data()] tiles = [task.get_tiles_json_data()]
else: else:
title = project.name title = project.name
tiles = project.get_tile_json_data() tiles = project.get_tiles_json_data()
print(tiles)
return render(request, 'app/map.html', { return render(request, 'app/map.html', {
'title': title, 'title': title,
'params': { 'params': {
'tiles': json.dumps(tiles) 'tiles': json.dumps(tiles),
'title': title
}.items() }.items()
}) })
@ -74,7 +77,7 @@ def model_display(request, project_pk=None, task_pk=None):
raise Http404() raise Http404()
if task_pk is not None: if task_pk is not None:
task = get_object_or_404(Task.objects.defer('orthophoto_extent'), pk=task_pk, project=project) task = get_object_or_404(Task.objects.defer('orthophoto_extent', 'dsm_extent', 'dtm_extent'), pk=task_pk, project=project)
title = task.name title = task.name
else: else:
raise Http404() raise Http404()

Wyświetl plik

@ -133,7 +133,7 @@ georeferenced_model.csv | Point cloud in .CSV format.
### Download assets (raw path) ### Download assets (raw path)
`GET /api/projects/{project_id}/tasks/{task_id}/assets/{path}/` `GET /api/projects/{project_id}/tasks/{task_id}/assets/{path}`
After a task has been successfully processed, its assets are stored in a directory on the file system. This API call allows direct access to the files in that directory (by default: `WebODM/app/media/project/{project_id}/task/{task_id}/assets`). This can be useful to those applications that want to stream a `Potree` dataset, or render a textured 3D model on the fly. After a task has been successfully processed, its assets are stored in a directory on the file system. This API call allows direct access to the files in that directory (by default: `WebODM/app/media/project/{project_id}/task/{task_id}/assets`). This can be useful to those applications that want to stream a `Potree` dataset, or render a textured 3D model on the fly.