kopia lustrzana https://github.com/OpenDroneMap/WebODM
commit
88f03424d3
|
|
@ -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>[^/.]+)/(?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()),
|
||||
|
|
|
|||
|
|
@ -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_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 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
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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}}"
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
11
app/views.py
11
app/views.py
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue