kopia lustrzana https://github.com/OpenDroneMap/WebODM
Started adding support to display DSM, DTM map tiles
rodzic
3f7e56177b
commit
186f4bac5e
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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}}"
|
||||
|
|
13
app/views.py
13
app/views.py
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue