Fixed map view display, speed enhancements, tiles.json fixes, project map display working

pull/50/head
Piero Toffanin 2016-11-16 13:02:43 -05:00
rodzic e3fa5470ed
commit adf6626200
10 zmienionych plików z 104 dodań i 134 usunięć

Wyświetl plik

@ -1,13 +1,7 @@
from django.contrib.gis.db.models.functions import Envelope
from rest_framework import serializers, viewsets 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 app import models
from .tasks import TaskIDsSerializer from .tasks import TaskIDsSerializer
from .common import get_and_check_project, get_tiles_json
class ProjectSerializer(serializers.ModelSerializer): class ProjectSerializer(serializers.ModelSerializer):
@ -35,24 +29,3 @@ class ProjectViewSet(viewsets.ModelViewSet):
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
queryset = models.Project.objects.filter(deleting=False) queryset = models.Project.objects.filter(deleting=False)
ordering_fields = '__all__' 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)

Wyświetl plik

@ -1,7 +1,10 @@
import mimetypes import mimetypes
import os 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.core.exceptions import ObjectDoesNotExist
from django.db.models.functions import Cast
from django.http import HttpResponse from django.http import HttpResponse
from wsgiref.util import FileWrapper from wsgiref.util import FileWrapper
from rest_framework import status, serializers, viewsets, filters, exceptions, permissions, parsers from rest_framework import status, serializers, viewsets, filters, exceptions, permissions, parsers
@ -145,12 +148,12 @@ class TaskViewSet(viewsets.ViewSet):
class TaskNestedView(APIView): 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) get_and_check_project(request, project_pk)
try: 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: except ObjectDoesNotExist:
raise exceptions.NotFound() raise exceptions.NotFound()
return task return task
@ -161,7 +164,7 @@ class TaskTiles(TaskNestedView):
""" """
Returns a prerendered orthophoto tile for a task 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) tile_path = task.get_tile_path(z, x, y)
if os.path.isfile(tile_path): if os.path.isfile(tile_path):
tile = open(tile_path, "rb") tile = open(tile_path, "rb")
@ -175,10 +178,12 @@ class TaskTilesJson(TaskNestedView):
""" """
Returns a tiles.json file for consumption by a client 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, [ json = get_tiles_json(task.name, [
'/api/projects/{}/tasks/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id) '/api/projects/{}/tasks/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id)
], task.orthophoto.extent) ], task.orthophoto_area.extent)
return Response(json) return Response(json)
@ -187,7 +192,7 @@ class TaskAssets(TaskNestedView):
""" """
Downloads a task asset (if available) 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 = { allowed_assets = {
'all': 'all.zip', 'all': 'all.zip',

Wyświetl plik

@ -1,5 +1,5 @@
from django.conf.urls import url, include from django.conf.urls import url, include
from .projects import ProjectViewSet, ProjectTilesJson from .projects import ProjectViewSet
from .tasks import TaskViewSet, TaskTiles, TaskTilesJson, TaskAssets from .tasks import TaskViewSet, TaskTiles, TaskTilesJson, TaskAssets
from .processingnodes import ProcessingNodeViewSet from .processingnodes import ProcessingNodeViewSet
from rest_framework_nested import routers from rest_framework_nested import routers
@ -15,8 +15,6 @@ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^', include(tasks_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/(?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\.json$', TaskTilesJson.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>[^/.]+)/$', TaskAssets.as_view()), url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>[^/.]+)/$', TaskAssets.as_view()),

Wyświetl plik

@ -64,6 +64,11 @@ class Project(models.Model):
def tasks(self): def tasks(self):
return self.task_set.only('id') 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: class Meta:
permissions = ( permissions = (
('view_project', 'Can view project'), ('view_project', 'Can view project'),
@ -326,6 +331,18 @@ class Task(models.Model):
def get_tile_path(self, z, x, y): def get_tile_path(self, z, x, y):
return self.assets_path("orthophoto_tiles", z, x, "{}.png".format(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): def delete(self, using=None, keep_parents=False):
directory_to_delete = os.path.join(settings.MEDIA_ROOT, directory_to_delete = os.path.join(settings.MEDIA_ROOT,
task_directory_path(self.id, self.project.id)) task_directory_path(self.id, self.project.id))

Wyświetl plik

@ -5,32 +5,16 @@ import AssetDownloadButtons from './components/AssetDownloadButtons';
class MapView extends React.Component { class MapView extends React.Component {
static defaultProps = { static defaultProps = {
task: "", tiles: []
project: ""
}; };
static propTypes = { static propTypes = {
// task id to display, if empty display all for a particular project tiles: React.PropTypes.array.isRequired // tiles.json list
task: React.PropTypes.string,
// project id to display, if empty display all
project: React.PropTypes.string
}; };
constructor(props){ constructor(props){
super(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 = { this.state = {
opacity: 100 opacity: 100
}; };
@ -40,7 +24,7 @@ class MapView extends React.Component {
updateOpacity(evt) { updateOpacity(evt) {
this.setState({ this.setState({
opacity: evt.target.value, opacity: parseFloat(evt.target.value),
}); });
} }
@ -48,12 +32,9 @@ class MapView extends React.Component {
const { opacity } = this.state; const { opacity } = this.state;
return (<div className="map-view"> 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="row controls">
<div className="col-md-3"> <div className="col-md-12 text-right">
<AssetDownloadButtons task={{id: this.props.task, project: this.props.project}} />
</div>
<div className="col-md-9 text-right">
Orthophoto opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} /> Orthophoto opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} />
</div> </div>
</div> </div>

Wyświetl plik

@ -4,12 +4,14 @@ import '../css/AssetDownloadButtons.scss';
class AssetDownloadButtons extends React.Component { class AssetDownloadButtons extends React.Component {
static defaultProps = { static defaultProps = {
disabled: false, disabled: false,
direction: "down", // or "up"
task: null task: null
}; };
static propTypes = { static propTypes = {
disabled: React.PropTypes.bool, disabled: React.PropTypes.bool,
task: React.PropTypes.object.isRequired task: React.PropTypes.object.isRequired,
direction: React.PropTypes.string
}; };
constructor(props){ constructor(props){
@ -26,7 +28,7 @@ class AssetDownloadButtons extends React.Component {
} }
render(){ 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"> <button type="button" className="btn btn-sm btn-primary" disabled={this.props.disabled} data-toggle="dropdown">
<i className="glyphicon glyphicon-download"></i> Download Assets <i className="glyphicon glyphicon-download"></i> Download Assets
</button> </button>

Wyświetl plik

@ -10,72 +10,36 @@ import ErrorMessage from './ErrorMessage';
class Map extends React.Component { class Map extends React.Component {
static defaultProps = { static defaultProps = {
bounds: [[-90, -180], [90, 180]],
maxzoom: 18, maxzoom: 18,
minzoom: 0, minzoom: 0,
scheme: 'tms',
showBackground: false, showBackground: false,
opacity: 100, opacity: 100
url: "", };
error: ""
}
static propTypes = { static propTypes = {
bounds: React.PropTypes.array, maxzoom: React.PropTypes.number,
maxzoom: React.PropTypes.integer, minzoom: React.PropTypes.number,
minzoom: React.PropTypes.integer,
scheme: React.PropTypes.string, // either 'tms' or 'xyz'
showBackground: React.PropTypes.bool, showBackground: React.PropTypes.bool,
showControls: React.PropTypes.bool, tiles: React.PropTypes.array.isRequired,
tileJSON: React.PropTypes.string, opacity: React.PropTypes.number
url: React.PropTypes.string };
}
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
bounds: this.props.bounds, error: "",
maxzoom: this.props.maxzoom, bounds: null
minzoom: this.props.minzoom
}; };
this.imageryLayers = [];
} }
componentDidMount() { componentDidMount() {
const { showBackground, tileJSON } = this.props; const { showBackground, tiles } = 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);
}
this.leaflet = Leaflet.map(this.container, { this.leaflet = Leaflet.map(this.container, {
scrollWheelZoom: true, scrollWheelZoom: true
layers
}); });
if (showBackground) { if (showBackground) {
@ -109,38 +73,63 @@ class Map extends React.Component {
})); }));
} }
this.leaflet.fitBounds(bounds); this.leaflet.fitWorld();
Leaflet.control.scale({ Leaflet.control.scale({
maxWidth: 250, maxWidth: 250,
}).addTo(this.leaflet); }).addTo(this.leaflet);
this.leaflet.attributionControl.setPrefix(""); 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() { componentDidUpdate() {
const { bounds, maxzoom, minzoom, scheme, url } = this.state; const { bounds } = this.state;
if (!this.imageryLayer) { if (bounds) this.leaflet.fitBounds(bounds);
this.imageryLayer = Leaflet.tileLayer(url, {
minZoom: minzoom, this.imageryLayers.forEach(imageryLayer => {
maxZoom: maxzoom, imageryLayer.setOpacity(this.props.opacity / 100);
tms: scheme === 'tms' });
}).addTo(this.leaflet);
this.leaflet.fitBounds(bounds);
}
this.imageryLayer.setOpacity(this.props.opacity / 100);
} }
componentWillUnmount() { componentWillUnmount() {
this.leaflet.remove(); this.leaflet.remove();
if (this.tileJsonRequest) this.tileJsonRequest.abort();
if (this.tileJsonRequests) {
this.tileJsonRequests.forEach(tileJsonRequest => this.tileJsonRequest.abort());
this.tileJsonRequests = [];
}
} }
render() { render() {
const { opacity, error } = this.state;
return ( return (
<div style={{height: "100%"}}> <div style={{height: "100%"}}>
<ErrorMessage bind={[this, 'error']} /> <ErrorMessage bind={[this, 'error']} />

Wyświetl plik

@ -197,7 +197,6 @@ class TestApi(BootTestCase):
# - tiles API urls (permissions, 404s) # - tiles API urls (permissions, 404s)
# - assets download # - assets download
# - project deletion # - project deletion
# - project tiles API urls
def test_processingnodes(self): def test_processingnodes(self):
client = APIClient() client = APIClient()

Wyświetl plik

@ -7,6 +7,8 @@ urlpatterns = [
url(r'^$', views.index, name='index'), url(r'^$', views.index, name='index'),
url(r'^dashboard/$', views.dashboard, name='dashboard'), 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>[^/.]+)/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'^processingnode/([\d]+)/$', views.processing_node, name='processing_node'),
url(r'^api/', include("app.api.urls")), url(r'^api/', include("app.api.urls")),

Wyświetl plik

@ -1,3 +1,5 @@
import json
from django.http import Http404 from django.http import Http404
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse from django.http import HttpResponse
@ -28,23 +30,25 @@ def dashboard(request):
@login_required @login_required
def map(request, project_pk=None, task_pk=None): def map(request, project_pk=None, task_pk=None):
title = _("Map") title = _("Map")
tiles = []
if project_pk != '': if project_pk is not None:
project = get_object_or_404(Project, pk=project_pk) project = get_object_or_404(Project, pk=project_pk)
if not request.user.has_perm('projects.view_project', project): if not request.user.has_perm('projects.view_project', project):
raise Http404() raise Http404()
if task_pk != '': if task_pk is not None:
task = get_object_or_404(Task, pk=task_pk, project=project) task = get_object_or_404(Task.objects.defer('orthophoto'), pk=task_pk, project=project)
title = task.name title = task.name
tiles = [task.get_tiles_json_data()]
else: else:
title = project.name title = project.name
tiles = project.get_tiles_json_data()
return render(request, 'app/map.html', { return render(request, 'app/map.html', {
'title': title, 'title': title,
'params': { 'params': {
'task': task_pk, 'tiles': json.dumps(tiles)
'project': project_pk
}.items() }.items()
}) })