Merge pull request #237 from pierotofy/dem

Add support to display DSM, DTM map tiles
pull/242/head
Piero Toffanin 2017-07-14 08:59:42 -04:00 zatwierdzone przez GitHub
commit 88f03424d3
15 zmienionych plików z 357 dodań i 150 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>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles/(?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_map_items(self):
return [task.get_map_items() 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,27 @@ 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")),
'orthophoto_extent'),
(os.path.realpath(self.assets_path("odm_dem", "dsm.tif")),
'dsm_extent'),
(os.path.realpath(self.assets_path("odm_dem", "dtm.tif")),
'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
# self.field = GEOSGeometry(...)
setattr(self, 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 +482,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_map_items(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,21 +5,55 @@ import $ from 'jquery';
class MapView extends React.Component {
static defaultProps = {
tiles: []
mapItems: [],
selectedMapType: 'orthophoto',
title: ""
};
static propTypes = {
tiles: React.PropTypes.array.isRequired // tiles.json list
mapItems: 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,
selectedMapType: props.selectedMapType,
tiles: this.getTilesByMapType(props.selectedMapType)
};
this.updateOpacity = this.updateOpacity.bind(this);
this.getTilesByMapType = this.getTilesByMapType.bind(this);
this.handleMapTypeButton = this.handleMapTypeButton.bind(this);
}
getTilesByMapType(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 => {
mapItem.tiles.forEach(tile => {
if (tile.type === type) tiles.push({
url: tile.url,
meta: mapItem.meta
});
});
});
return tiles;
}
handleMapTypeButton(type){
return () => {
this.setState({
selectedMapType: type,
tiles: this.getTilesByMapType(type)
});
};
}
updateOpacity(evt) {
@ -30,9 +64,43 @@ class MapView extends React.Component {
render(){
const { opacity } = this.state;
let mapTypeButtons = [
{
label: "Orthophoto",
type: "orthophoto"
},
{
label: "Surface Model",
type: "dsm"
},
{
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 (<div className="map-view">
<Map tiles={this.props.tiles} showBackground={true} opacity={opacity}/>
<div className="map-type-selector btn-group" role="group">
{mapTypeButtons.map(mapType =>
<button
key={mapType.type}
onClick={this.handleMapTypeButton(mapType.type)}
className={"btn btn-sm " + (mapType.type === this.state.selectedMapType ? "btn-default" : "btn-secondary")}>{mapType.label}</button>
)}
</div>
{this.props.title ?
<h3><i className="fa fa-globe"></i> {this.props.title}</h3>
: ""}
<Map
tiles={this.state.tiles}
showBackground={true}
opacity={opacity}
mapType={this.state.selectedMapType} />
<div className="opacity-slider">
Opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} />
</div>

Wyświetl plik

@ -33,6 +33,8 @@ 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 (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) {
@ -43,18 +45,130 @@ class Map extends React.Component {
this.imageryLayers = [];
this.basemaps = {};
this.mapBounds = null;
this.autolayers = null;
this.loadImageryLayers = this.loadImageryLayers.bind(this);
}
loadImageryLayers(forceAddLayers = false){
const { tiles } = this.props,
assets = AssetDownloads.excludeSeparators(),
layerId = layer => {
const meta = layer[Symbol.for("meta")];
return meta.project + "_" + meta.task;
};
// Remove all previous imagery layers
// 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 = [];
async.each(tiles, (tile, done) => {
const { url, meta } = tile;
this.tileJsonRequests.push($.getJSON(url)
.done(info => {
const bounds = Leaflet.latLngBounds(
[info.bounds.slice(0, 2).reverse(), info.bounds.slice(2, 4).reverse()]
);
const layer = Leaflet.tileLayer(info.tiles[0], {
bounds,
minZoom: info.minzoom,
maxZoom: info.maxzoom,
tms: info.scheme === 'tms',
opacity: this.props.opacity / 100
});
// 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,
project: meta.project
};
if (tiles.length === 1){
this.setState({switchButtonTask: task});
}
// For some reason, getLatLng is not defined for tileLayer?
// We need this function if other code calls layer.openPopup()
layer.getLatLng = function(){
return this.options.bounds.getCenter();
};
layer.bindPopup(`<div class="title">${info.name}</div>
<div>Bounds: [${layer.options.bounds.toBBoxString().split(",").join(", ")}]</div>
<ul class="asset-links">
${assets.map(asset => {
return `<li><a href="${asset.downloadUrl(meta.project, meta.task)}">${asset.label}</a></li>`;
}).join("")}
</ul>
<button
onclick="location.href='/3d/project/${task.project}/task/${task.id}/';"
type="button"
class="switchModeButton btn btn-sm btn-default btn-white">
<i class="fa fa-cube"></i> 3D
</button>
`);
this.imageryLayers.push(layer);
let mapBounds = this.mapBounds || Leaflet.latLngBounds();
mapBounds.extend(bounds);
this.mapBounds = mapBounds;
// Add layer to layers control
this.autolayers.addOverlay(layer, info.name);
done();
})
.fail((_, __, err) => done(err))
);
}, err => {
if (err){
this.setState({error: err.message || JSON.stringify(err)});
reject(err);
}else{
resolve();
}
});
});
}
componentDidMount() {
const { showBackground, tiles } = this.props;
const assets = AssetDownloads.excludeSeparators();
const { showBackground } = this.props;
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}', {
@ -79,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({
@ -86,88 +206,9 @@ class Map extends React.Component {
}).addTo(this.map);
this.map.attributionControl.setPrefix("");
this.tileJsonRequests = [];
async.each(tiles, (tile, done) => {
const { url, meta } = tile;
this.tileJsonRequests.push($.getJSON(url)
.done(info => {
const bounds = Leaflet.latLngBounds(
[info.bounds.slice(0, 2).reverse(), info.bounds.slice(2, 4).reverse()]
);
const layer = Leaflet.tileLayer(info.tiles[0], {
bounds,
minZoom: info.minzoom,
maxZoom: info.maxzoom,
tms: info.scheme === 'tms'
}).addTo(this.map);
// Associate metadata with this layer
meta.name = info.name;
layer[Symbol.for("meta")] = meta;
// Show 3D switch button only if we have a single orthophoto
const task = {
id: meta.task,
project: meta.project
};
if (tiles.length === 1){
this.setState({switchButtonTask: task});
}
// For some reason, getLatLng is not defined for tileLayer?
// We need this function if other code calls layer.openPopup()
layer.getLatLng = function(){
return this.options.bounds.getCenter();
};
layer.bindPopup(`<div class="title">${info.name}</div>
<div>Bounds: [${layer.options.bounds.toBBoxString().split(",").join(", ")}]</div>
<ul class="asset-links">
${assets.map(asset => {
return `<li><a href="${asset.downloadUrl(meta.project, meta.task)}">${asset.label}</a></li>`;
}).join("")}
</ul>
<button
onclick="location.href='/3d/project/${task.project}/task/${task.id}/';"
type="button"
class="switchModeButton btn btn-sm btn-default btn-white">
<i class="fa fa-cube"></i> 3D
</button>
`);
this.imageryLayers.push(layer);
let mapBounds = this.mapBounds || Leaflet.latLngBounds();
mapBounds.extend(bounds);
this.mapBounds = mapBounds;
done();
})
.fail((_, __, err) => done(err))
);
}, err => {
if (err) this.setState({error: err.message || JSON.stringify(err)});
else{
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){
@ -177,14 +218,19 @@ class Map extends React.Component {
}
}
});
}
});
}
componentDidUpdate() {
componentDidUpdate(prevProps) {
this.imageryLayers.forEach(imageryLayer => {
imageryLayer.setOpacity(this.props.opacity / 100);
});
if (prevProps.tiles !== this.props.tiles){
this.loadImageryLayers().then(() => {
// console.log("GOT: ", this.autolayers, this.autolayers.selectedOverlays);
});
}
}
componentWillUnmount() {

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

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

Wyświetl plik

@ -49,17 +49,18 @@ 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()]
mapItems = [task.get_map_items()]
else:
title = project.name
tiles = project.get_tile_json_data()
mapItems = project.get_map_items()
return render(request, 'app/map.html', {
'title': title,
'params': {
'tiles': json.dumps(tiles)
'map-items': json.dumps(mapItems),
'title': title
}.items()
})
@ -74,7 +75,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()

@ -1 +1 @@
Subproject commit bfc90c9cec21a06b88ed92f202a0a79901b8962d
Subproject commit 3586a31f9071db5931b52c580bbf009f25e3b5ec

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.
@ -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).
<aside class="notice">If you use <a href="http://leafletjs.com/" target="_blank">Leaflet</a>, you'll need to pass the authentication token via querystring: /api/projects/{project_id}/tasks/{task_id}/tiles/{Z}/{X}/{Y}.png?jwt=your_token</aside>
### 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.