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:
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)

Wyświetl plik

@ -18,8 +18,9 @@ urlpatterns = [
url(r'^', include(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\.json$', TaskTilesJson.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>[^/.]+)/(?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>[^/.]+)/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.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 models 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 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.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

Wyświetl plik

@ -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: <tiles.json>},
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 (<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}/>
<div className="opacity-slider">
Opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} />

Wyświetl plik

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

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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{

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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',

Wyświetl plik

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

Wyświetl plik

@ -49,17 +49,20 @@ def map(request, project_pk=None, task_pk=None):
raise Http404()
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
tiles = [task.get_tile_json_data()]
tiles = [task.get_tiles_json_data()]
else:
title = project.name
tiles = project.get_tile_json_data()
tiles = project.get_tiles_json_data()
print(tiles)
return render(request, 'app/map.html', {
'title': title,
'params': {
'tiles': json.dumps(tiles)
'tiles': json.dumps(tiles),
'title': title
}.items()
})
@ -74,7 +77,7 @@ def model_display(request, project_pk=None, task_pk=None):
raise Http404()
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
else:
raise Http404()

Wyświetl plik

@ -133,7 +133,7 @@ georeferenced_model.csv | Point cloud in .CSV format.
### 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.