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

Wyświetl plik

@ -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',

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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']} />

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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