kopia lustrzana https://github.com/OpenDroneMap/WebODM
Fixed map view display, speed enhancements, tiles.json fixes, project map display working
rodzic
e3fa5470ed
commit
adf6626200
|
@ -1,13 +1,7 @@
|
|||
from django.contrib.gis.db.models.functions import Envelope
|
||||
from rest_framework import serializers, viewsets
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from django.contrib.gis.db.models import Extent
|
||||
|
||||
from app import models
|
||||
from .tasks import TaskIDsSerializer
|
||||
from .common import get_and_check_project, get_tiles_json
|
||||
|
||||
|
||||
class ProjectSerializer(serializers.ModelSerializer):
|
||||
|
@ -35,24 +29,3 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = ProjectSerializer
|
||||
queryset = models.Project.objects.filter(deleting=False)
|
||||
ordering_fields = '__all__'
|
||||
|
||||
|
||||
class ProjectTilesJson(APIView):
|
||||
queryset = models.Project.objects.filter(deleting=False)
|
||||
|
||||
def get(self, request, pk=None):
|
||||
"""
|
||||
Returns a tiles.json file for consumption by a client
|
||||
"""
|
||||
project = get_and_check_project(request, pk)
|
||||
task_ids = [task.id for task in project.tasks()]
|
||||
extent = [0, 0, 0, 0] # TODO! world extent
|
||||
|
||||
if len(task_ids) > 0:
|
||||
# Extent of all orthophotos of all tasks for this project
|
||||
extent = project.task_set.only('geom').annotate(geom=Envelope('orthophoto')).aggregate(Extent('geom'))['geom__extent']
|
||||
|
||||
json = get_tiles_json(project.name, [
|
||||
'/api/projects/{}/tasks/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(project.id, task_id) for task_id in task_ids
|
||||
], extent)
|
||||
return Response(json)
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import mimetypes
|
||||
import os
|
||||
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
from django.contrib.gis.db.models.functions import Envelope
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models.functions import Cast
|
||||
from django.http import HttpResponse
|
||||
from wsgiref.util import FileWrapper
|
||||
from rest_framework import status, serializers, viewsets, filters, exceptions, permissions, parsers
|
||||
|
@ -145,12 +148,12 @@ class TaskViewSet(viewsets.ViewSet):
|
|||
|
||||
|
||||
class TaskNestedView(APIView):
|
||||
queryset = models.Task.objects.all()
|
||||
queryset = models.Task.objects.all().defer('orthophoto', 'console_output')
|
||||
|
||||
def get_and_check_task(self, request, pk, project_pk, defer=(None, )):
|
||||
def get_and_check_task(self, request, pk, project_pk, annotate={}):
|
||||
get_and_check_project(request, project_pk)
|
||||
try:
|
||||
task = self.queryset.defer(*defer).get(pk=pk, project=project_pk)
|
||||
task = self.queryset.annotate(**annotate).get(pk=pk, project=project_pk)
|
||||
except ObjectDoesNotExist:
|
||||
raise exceptions.NotFound()
|
||||
return task
|
||||
|
@ -161,7 +164,7 @@ class TaskTiles(TaskNestedView):
|
|||
"""
|
||||
Returns a prerendered orthophoto tile for a task
|
||||
"""
|
||||
task = self.get_and_check_task(request, pk, project_pk, ('orthophoto', 'console_output'))
|
||||
task = self.get_and_check_task(request, pk, project_pk)
|
||||
tile_path = task.get_tile_path(z, x, y)
|
||||
if os.path.isfile(tile_path):
|
||||
tile = open(tile_path, "rb")
|
||||
|
@ -175,10 +178,12 @@ class TaskTilesJson(TaskNestedView):
|
|||
"""
|
||||
Returns a tiles.json file for consumption by a client
|
||||
"""
|
||||
task = self.get_and_check_task(request, pk, project_pk)
|
||||
task = self.get_and_check_task(request, pk, project_pk, annotate={
|
||||
'orthophoto_area': Envelope(Cast("orthophoto", GeometryField()))
|
||||
})
|
||||
json = get_tiles_json(task.name, [
|
||||
'/api/projects/{}/tasks/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id)
|
||||
], task.orthophoto.extent)
|
||||
], task.orthophoto_area.extent)
|
||||
return Response(json)
|
||||
|
||||
|
||||
|
@ -187,7 +192,7 @@ class TaskAssets(TaskNestedView):
|
|||
"""
|
||||
Downloads a task asset (if available)
|
||||
"""
|
||||
task = self.get_and_check_task(request, pk, project_pk, ('orthophoto', 'console_output'))
|
||||
task = self.get_and_check_task(request, pk, project_pk)
|
||||
|
||||
allowed_assets = {
|
||||
'all': 'all.zip',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.conf.urls import url, include
|
||||
from .projects import ProjectViewSet, ProjectTilesJson
|
||||
from .projects import ProjectViewSet
|
||||
from .tasks import TaskViewSet, TaskTiles, TaskTilesJson, TaskAssets
|
||||
from .processingnodes import ProcessingNodeViewSet
|
||||
from rest_framework_nested import routers
|
||||
|
@ -15,8 +15,6 @@ urlpatterns = [
|
|||
url(r'^', include(router.urls)),
|
||||
url(r'^', include(tasks_router.urls)),
|
||||
|
||||
url(r'projects/(?P<pk>[^/.]+)/tiles\.json$', ProjectTilesJson.as_view()),
|
||||
|
||||
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>[^/.]+)/download/(?P<asset>[^/.]+)/$', TaskAssets.as_view()),
|
||||
|
|
|
@ -64,6 +64,11 @@ class Project(models.Model):
|
|||
def tasks(self):
|
||||
return self.task_set.only('id')
|
||||
|
||||
def get_tiles_json_data(self):
|
||||
return [task.get_tiles_json_data() for task in self.task_set.filter(
|
||||
status=status_codes.COMPLETED
|
||||
).only('id', 'project_id')]
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_project', 'Can view project'),
|
||||
|
@ -326,6 +331,18 @@ class Task(models.Model):
|
|||
def get_tile_path(self, z, x, y):
|
||||
return self.assets_path("orthophoto_tiles", z, x, "{}.png".format(y))
|
||||
|
||||
def get_tiles_json_url(self):
|
||||
return "/api/projects/{}/tasks/{}/tiles.json".format(self.project.id, self.id)
|
||||
|
||||
def get_tiles_json_data(self):
|
||||
return {
|
||||
'url': self.get_tiles_json_url(),
|
||||
'meta': {
|
||||
'task': self.id,
|
||||
'project': self.project.id
|
||||
}
|
||||
}
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
directory_to_delete = os.path.join(settings.MEDIA_ROOT,
|
||||
task_directory_path(self.id, self.project.id))
|
||||
|
|
|
@ -5,32 +5,16 @@ import AssetDownloadButtons from './components/AssetDownloadButtons';
|
|||
|
||||
class MapView extends React.Component {
|
||||
static defaultProps = {
|
||||
task: "",
|
||||
project: ""
|
||||
tiles: []
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
// task id to display, if empty display all for a particular project
|
||||
task: React.PropTypes.string,
|
||||
|
||||
// project id to display, if empty display all
|
||||
project: React.PropTypes.string
|
||||
tiles: React.PropTypes.array.isRequired // tiles.json list
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.tileJSON = "";
|
||||
if (this.props.project === ""){
|
||||
this.tileJSON = "/api/projects/tiles.json"
|
||||
throw new Error("TODO: not built yet");
|
||||
}else if (this.props.task === ""){
|
||||
this.tileJSON = `/api/projects/${this.props.project}/tasks/tiles.json`;
|
||||
throw new Error("TODO: not built yet");
|
||||
}else{
|
||||
this.tileJSON = `/api/projects/${this.props.project}/tasks/${this.props.task}/tiles.json`;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
opacity: 100
|
||||
};
|
||||
|
@ -40,7 +24,7 @@ class MapView extends React.Component {
|
|||
|
||||
updateOpacity(evt) {
|
||||
this.setState({
|
||||
opacity: evt.target.value,
|
||||
opacity: parseFloat(evt.target.value),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -48,12 +32,9 @@ class MapView extends React.Component {
|
|||
const { opacity } = this.state;
|
||||
|
||||
return (<div className="map-view">
|
||||
<Map tileJSON={this.tileJSON} showBackground={true} opacity={opacity}/>
|
||||
<Map tiles={this.props.tiles} showBackground={true} opacity={opacity}/>
|
||||
<div className="row controls">
|
||||
<div className="col-md-3">
|
||||
<AssetDownloadButtons task={{id: this.props.task, project: this.props.project}} />
|
||||
</div>
|
||||
<div className="col-md-9 text-right">
|
||||
<div className="col-md-12 text-right">
|
||||
Orthophoto opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,12 +4,14 @@ import '../css/AssetDownloadButtons.scss';
|
|||
class AssetDownloadButtons extends React.Component {
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
direction: "down", // or "up"
|
||||
task: null
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
disabled: React.PropTypes.bool,
|
||||
task: React.PropTypes.object.isRequired
|
||||
task: React.PropTypes.object.isRequired,
|
||||
direction: React.PropTypes.string
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
|
@ -26,7 +28,7 @@ class AssetDownloadButtons extends React.Component {
|
|||
}
|
||||
|
||||
render(){
|
||||
return (<div className="asset-download-buttons btn-group">
|
||||
return (<div className={"asset-download-buttons btn-group " + (this.props.direction === "up" ? "dropup" : "")}>
|
||||
<button type="button" className="btn btn-sm btn-primary" disabled={this.props.disabled} data-toggle="dropdown">
|
||||
<i className="glyphicon glyphicon-download"></i> Download Assets
|
||||
</button>
|
||||
|
|
|
@ -10,72 +10,36 @@ import ErrorMessage from './ErrorMessage';
|
|||
|
||||
class Map extends React.Component {
|
||||
static defaultProps = {
|
||||
bounds: [[-90, -180], [90, 180]],
|
||||
maxzoom: 18,
|
||||
minzoom: 0,
|
||||
scheme: 'tms',
|
||||
showBackground: false,
|
||||
opacity: 100,
|
||||
url: "",
|
||||
error: ""
|
||||
}
|
||||
opacity: 100
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
bounds: React.PropTypes.array,
|
||||
maxzoom: React.PropTypes.integer,
|
||||
minzoom: React.PropTypes.integer,
|
||||
scheme: React.PropTypes.string, // either 'tms' or 'xyz'
|
||||
maxzoom: React.PropTypes.number,
|
||||
minzoom: React.PropTypes.number,
|
||||
showBackground: React.PropTypes.bool,
|
||||
showControls: React.PropTypes.bool,
|
||||
tileJSON: React.PropTypes.string,
|
||||
url: React.PropTypes.string
|
||||
}
|
||||
tiles: React.PropTypes.array.isRequired,
|
||||
opacity: React.PropTypes.number
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
bounds: this.props.bounds,
|
||||
maxzoom: this.props.maxzoom,
|
||||
minzoom: this.props.minzoom
|
||||
error: "",
|
||||
bounds: null
|
||||
};
|
||||
|
||||
this.imageryLayers = [];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { showBackground, tileJSON } = this.props;
|
||||
const { bounds, maxzoom, minzoom, scheme, url } = this.state;
|
||||
|
||||
if (tileJSON != null) {
|
||||
this.tileJsonRequest = $.getJSON(tileJSON)
|
||||
.done(info => {
|
||||
const bounds = [info.bounds.slice(0, 2).reverse(), info.bounds.slice(2, 4).reverse()];
|
||||
|
||||
this.setState({
|
||||
bounds,
|
||||
maxzoom: info.maxzoom,
|
||||
minzoom: info.minzoom,
|
||||
scheme: info.scheme || 'xyz',
|
||||
url: info.tiles[0]
|
||||
});
|
||||
})
|
||||
.fail((_, __, err) => this.setState({error: err.message}));
|
||||
}
|
||||
|
||||
const layers = [];
|
||||
|
||||
if (url != null) {
|
||||
this.imageryLayer = Leaflet.tileLayer(url, {
|
||||
minZoom: minzoom,
|
||||
maxZoom: maxzoom,
|
||||
tms: scheme === 'tms'
|
||||
});
|
||||
|
||||
layers.push(this.imageryLayer);
|
||||
}
|
||||
const { showBackground, tiles } = this.props;
|
||||
|
||||
this.leaflet = Leaflet.map(this.container, {
|
||||
scrollWheelZoom: true,
|
||||
layers
|
||||
scrollWheelZoom: true
|
||||
});
|
||||
|
||||
if (showBackground) {
|
||||
|
@ -109,38 +73,63 @@ class Map extends React.Component {
|
|||
}));
|
||||
}
|
||||
|
||||
this.leaflet.fitBounds(bounds);
|
||||
this.leaflet.fitWorld();
|
||||
|
||||
Leaflet.control.scale({
|
||||
maxWidth: 250,
|
||||
}).addTo(this.leaflet);
|
||||
this.leaflet.attributionControl.setPrefix("");
|
||||
|
||||
this.tileJsonRequests = [];
|
||||
|
||||
tiles.forEach(tile => {
|
||||
const { url, meta } = tile;
|
||||
|
||||
this.tileJsonRequests.push($.getJSON(url)
|
||||
.done(info => {
|
||||
const bounds = [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.leaflet);
|
||||
|
||||
// Associate metadata with this layer
|
||||
layer[Symbol.for("meta")] = meta;
|
||||
|
||||
this.imageryLayers.push(layer);
|
||||
|
||||
let mapBounds = this.state.bounds || Leaflet.latLngBounds(bounds);
|
||||
mapBounds.extend(bounds);
|
||||
this.setState({bounds: mapBounds});
|
||||
})
|
||||
.fail((_, __, err) => this.setState({error: err.message}))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { bounds, maxzoom, minzoom, scheme, url } = this.state;
|
||||
const { bounds } = this.state;
|
||||
|
||||
if (!this.imageryLayer) {
|
||||
this.imageryLayer = Leaflet.tileLayer(url, {
|
||||
minZoom: minzoom,
|
||||
maxZoom: maxzoom,
|
||||
tms: scheme === 'tms'
|
||||
}).addTo(this.leaflet);
|
||||
|
||||
this.leaflet.fitBounds(bounds);
|
||||
}
|
||||
|
||||
this.imageryLayer.setOpacity(this.props.opacity / 100);
|
||||
if (bounds) this.leaflet.fitBounds(bounds);
|
||||
|
||||
this.imageryLayers.forEach(imageryLayer => {
|
||||
imageryLayer.setOpacity(this.props.opacity / 100);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.leaflet.remove();
|
||||
if (this.tileJsonRequest) this.tileJsonRequest.abort();
|
||||
|
||||
if (this.tileJsonRequests) {
|
||||
this.tileJsonRequests.forEach(tileJsonRequest => this.tileJsonRequest.abort());
|
||||
this.tileJsonRequests = [];
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { opacity, error } = this.state;
|
||||
|
||||
return (
|
||||
<div style={{height: "100%"}}>
|
||||
<ErrorMessage bind={[this, 'error']} />
|
||||
|
|
|
@ -197,7 +197,6 @@ class TestApi(BootTestCase):
|
|||
# - tiles API urls (permissions, 404s)
|
||||
# - assets download
|
||||
# - project deletion
|
||||
# - project tiles API urls
|
||||
|
||||
def test_processingnodes(self):
|
||||
client = APIClient()
|
||||
|
|
|
@ -7,6 +7,8 @@ urlpatterns = [
|
|||
url(r'^$', views.index, name='index'),
|
||||
url(r'^dashboard/$', views.dashboard, name='dashboard'),
|
||||
url(r'^map/project/(?P<project_pk>[^/.]+)/task/(?P<task_pk>[^/.]+)/$', views.map, name='map'),
|
||||
url(r'^map/project/(?P<project_pk>[^/.]+)/$', views.map, name='map'),
|
||||
|
||||
url(r'^processingnode/([\d]+)/$', views.processing_node, name='processing_node'),
|
||||
|
||||
url(r'^api/', include("app.api.urls")),
|
||||
|
|
14
app/views.py
14
app/views.py
|
@ -1,3 +1,5 @@
|
|||
import json
|
||||
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
|
@ -28,23 +30,25 @@ def dashboard(request):
|
|||
@login_required
|
||||
def map(request, project_pk=None, task_pk=None):
|
||||
title = _("Map")
|
||||
tiles = []
|
||||
|
||||
if project_pk != '':
|
||||
if project_pk is not None:
|
||||
project = get_object_or_404(Project, pk=project_pk)
|
||||
if not request.user.has_perm('projects.view_project', project):
|
||||
raise Http404()
|
||||
|
||||
if task_pk != '':
|
||||
task = get_object_or_404(Task, pk=task_pk, project=project)
|
||||
if task_pk is not None:
|
||||
task = get_object_or_404(Task.objects.defer('orthophoto'), pk=task_pk, project=project)
|
||||
title = task.name
|
||||
tiles = [task.get_tiles_json_data()]
|
||||
else:
|
||||
title = project.name
|
||||
tiles = project.get_tiles_json_data()
|
||||
|
||||
return render(request, 'app/map.html', {
|
||||
'title': title,
|
||||
'params': {
|
||||
'task': task_pk,
|
||||
'project': project_pk
|
||||
'tiles': json.dumps(tiles)
|
||||
}.items()
|
||||
})
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue