Added layer list on map display, popup info, download links on map, measurement tool

pull/50/head
Piero Toffanin 2016-11-17 18:51:07 -05:00
rodzic 2d1bda4017
commit 1dec6ba089
37 zmienionych plików z 2070 dodań i 85 usunięć

Wyświetl plik

@ -28,14 +28,11 @@ class ProcessingNodeFilter(FilterSet):
class ProcessingNodeViewSet(viewsets.ModelViewSet):
"""
Processing nodes available. Processing nodes are associated with
zero or more tasks and take care of processing input images.
Processing node get/add/delete/update
Processing nodes are associated with zero or more tasks and
take care of processing input images.
"""
# Don't need a "view node" permission. If you are logged-in, you can view nodes.
permission_classes = (DjangoModelPermissions, )
filter_backends = (DjangoFilterBackend, )
filter_class = ProcessingNodeFilter
pagination_class = None

Wyświetl plik

@ -18,12 +18,11 @@ class ProjectSerializer(serializers.ModelSerializer):
class ProjectViewSet(viewsets.ModelViewSet):
"""
Projects the current user has access to. Projects are the building blocks
Project get/add/delete/update
Projects are the building blocks
of processing. Each project can have zero or more tasks associated with it.
Users can fine tune the permissions on projects, including whether users/groups have
access to view, add, change or delete them.<br/><br/>
- /api/projects/&lt;projectId&gt;/tasks : list all tasks belonging to a project<br/>
- /api/projects/&lt;projectId&gt;/tasks/&lt;taskId&gt; : get task details
access to view, add, change or delete them.
"""
filter_fields = ('id', 'name', 'description', 'created_at')
serializer_class = ProjectSerializer

Wyświetl plik

@ -37,6 +37,7 @@ class TaskSerializer(serializers.ModelSerializer):
class TaskViewSet(viewsets.ViewSet):
"""
Task get/add/delete/update
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.
"""
@ -162,7 +163,7 @@ class TaskNestedView(APIView):
class TaskTiles(TaskNestedView):
def get(self, request, pk=None, project_pk=None, z="", x="", y=""):
"""
Returns a prerendered orthophoto tile for a task
Get an orthophoto tile
"""
task = self.get_and_check_task(request, pk, project_pk)
tile_path = task.get_tile_path(z, x, y)
@ -176,7 +177,7 @@ class TaskTiles(TaskNestedView):
class TaskTilesJson(TaskNestedView):
def get(self, request, pk=None, project_pk=None):
"""
Returns a tiles.json file for consumption by a client
Get tiles.json for this tasks's orthophoto
"""
task = self.get_and_check_task(request, pk, project_pk, annotate={
'orthophoto_area': Envelope(Cast("orthophoto", GeometryField()))

Wyświetl plik

@ -21,6 +21,9 @@ def boot():
default_group.permissions.add(
*list(Permission.objects.filter(codename__endswith=permission))
)
# Add permission to view processing nodes
default_group.permissions.add(Permission.objects.get(codename="view_processingnode"))
# Check super user
if User.objects.filter(is_superuser=True).count() == 0:

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -35,7 +35,7 @@ class MapView extends React.Component {
<Map tiles={this.props.tiles} showBackground={true} opacity={opacity}/>
<div className="row controls">
<div className="col-md-12 text-right">
Orthophoto opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} />
Orthophotos opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} />
</div>
</div>
</div>);

Wyświetl plik

@ -0,0 +1,49 @@
class AssetDownload{
constructor(label, asset, icon){
this.label = label;
this.asset = asset;
this.icon = icon;
}
downloadUrl(project_id, task_id){
return `/api/projects/${project_id}/tasks/${task_id}/download/${this.asset}/`;
}
get separator(){
return false;
}
}
class AssetDownloadSeparator extends AssetDownload{
constructor(){
super("-");
}
downloadUrl(){
return "#";
}
get separator(){
return true;
}
}
const api = {
all: function() {
return [
new AssetDownload("GeoTIFF","geotiff","fa fa-map-o"),
new AssetDownload("LAS","las","fa fa-cube"),
new AssetDownload("PLY","ply","fa fa-cube"),
new AssetDownload("CSV","csv","fa fa-cube"),
new AssetDownloadSeparator(),
new AssetDownload("All Assets","all","fa fa-file-archive-o")
];
},
excludeSeparators: function(){
return api.all().filter(asset => !asset.separator);
}
}
export default api;

Wyświetl plik

@ -1,5 +1,6 @@
import React from 'react';
import '../css/AssetDownloadButtons.scss';
import AssetDownloads from '../classes/AssetDownloads';
class AssetDownloadButtons extends React.Component {
static defaultProps = {
@ -20,14 +21,17 @@ class AssetDownloadButtons extends React.Component {
this.downloadAsset = this.downloadAsset.bind(this);
}
downloadAsset(type){
downloadAsset(asset){
return (e) => {
e.preventDefault();
location.href = `/api/projects/${this.props.task.project}/tasks/${this.props.task.id}/download/${type}/`;
location.href = asset.downloadUrl(this.props.task.project, this.props.task.id)
};
}
render(){
const assetDownloads = AssetDownloads.all();
let i = 0;
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
@ -36,12 +40,15 @@ class AssetDownloadButtons extends React.Component {
<span className="caret"></span>
</button>
<ul className="dropdown-menu">
<li><a href="javascript:void(0);" onClick={this.downloadAsset("geotiff")}><i className="fa fa-map-o"></i> GeoTIFF</a></li>
<li><a href="javascript:void(0);" onClick={this.downloadAsset("las")}><i className="fa fa-cube"></i> LAS</a></li>
<li><a href="javascript:void(0);" onClick={this.downloadAsset("ply")}><i className="fa fa-cube"></i> PLY</a></li>
<li><a href="javascript:void(0);" onClick={this.downloadAsset("ply")}><i className="fa fa-cube"></i> CSV</a></li>
<li className="divider"></li>
<li><a href="javascript:void(0);" onClick={this.downloadAsset("all")}><i className="fa fa-file-archive-o"></i> All Assets</a></li>
{assetDownloads.map(asset => {
if (!asset.separator){
return (<li key={i++}>
<a href="javascript:void(0);" onClick={this.downloadAsset(asset)}><i className={asset.icon}></i> {asset.label}</a>
</li>);
}else{
return (<li key={i++} className="divider"></li>);
}
})}
</ul>
</div>);
}

Wyświetl plik

@ -1,12 +1,16 @@
import React from 'react';
import '../css/Map.scss';
//import 'leaflet.css';
import 'leaflet/dist/leaflet.css';
import 'leaflet-basemaps/L.Control.Basemaps.css';
import Leaflet from 'leaflet';
import 'leaflet-basemaps/L.Control.Basemaps';
import 'leaflet-measure/dist/leaflet-measure.css';
import 'leaflet-measure/dist/leaflet-measure';
import '../vendor/leaflet/L.Control.MousePosition.css';
import '../vendor/leaflet/L.Control.MousePosition';
import '../vendor/leaflet/Leaflet.Autolayers/css/leaflet.auto-layers.css';
import '../vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers';
import $ from 'jquery';
import ErrorMessage from './ErrorMessage';
import AssetDownloads from '../classes/AssetDownloads';
class Map extends React.Component {
static defaultProps = {
@ -28,82 +32,124 @@ class Map extends React.Component {
super(props);
this.state = {
error: "",
bounds: null
error: ""
};
this.imageryLayers = [];
this.basemaps = {};
this.mapBounds = null;
}
componentDidMount() {
const { showBackground, tiles } = this.props;
const assets = AssetDownloads.excludeSeparators();
this.leaflet = Leaflet.map(this.container, {
scrollWheelZoom: true
this.map = Leaflet.map(this.container, {
scrollWheelZoom: true,
measureControl: true,
positionControl: true
});
if (showBackground) {
const basemaps = [
L.tileLayer('//{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
this.basemaps = {
"Google Maps Hybrid": L.tileLayer('//{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
attribution: 'Map data: &copy; Google Maps',
subdomains: ['mt0','mt1','mt2','mt3'],
maxZoom: 22,
minZoom: 0,
label: 'Google Maps Hybrid'
}),
L.tileLayer('//server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
}).addTo(this.map),
"ESRI Satellite": L.tileLayer('//server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
maxZoom: 22,
minZoom: 0,
label: 'ESRI Satellite' // optional label used for tooltip
}),
L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
"OSM Mapnik": L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 22,
minZoom: 0,
label: 'OSM Mapnik' // optional label used for tooltip
})
];
this.leaflet.addControl(Leaflet.control.basemaps({
basemaps: basemaps,
tileX: 0, // tile X coordinate
tileY: 0, // tile Y coordinate
tileZ: 1 // tile zoom level
}));
};
}
this.leaflet.fitWorld();
this.map.fitWorld();
Leaflet.control.scale({
maxWidth: 250,
}).addTo(this.leaflet);
this.leaflet.attributionControl.setPrefix("");
}).addTo(this.map);
this.map.attributionControl.setPrefix("");
this.tileJsonRequests = [];
let tilesLoaded = 0;
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 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.leaflet);
bounds,
minZoom: info.minzoom,
maxZoom: info.maxzoom,
tms: info.scheme === 'tms'
}).addTo(this.map);
// For some reason, getLatLng is not defined for tileLayer?
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>
`);
// Associate metadata with this layer
meta.name = info.name;
layer[Symbol.for("meta")] = meta;
this.imageryLayers.push(layer);
let mapBounds = this.state.bounds || Leaflet.latLngBounds(bounds);
let mapBounds = this.mapBounds || Leaflet.latLngBounds();
mapBounds.extend(bounds);
this.setState({bounds: mapBounds});
this.mapBounds = mapBounds;
// Done loading all tiles?
if (++tilesLoaded === tiles.length){
this.map.fitBounds(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){
if (layer._map && layer.options.bounds.contains(e.latlng)){
layer.openPopup();
break;
}
}
});
}
})
.fail((_, __, err) => this.setState({error: err.message}))
);
@ -111,17 +157,13 @@ class Map extends React.Component {
}
componentDidUpdate() {
const { bounds } = this.state;
if (bounds) this.leaflet.fitBounds(bounds);
this.imageryLayers.forEach(imageryLayer => {
imageryLayer.setOpacity(this.props.opacity / 100);
});
}
componentWillUnmount() {
this.leaflet.remove();
this.map.remove();
if (this.tileJsonRequests) {
this.tileJsonRequests.forEach(tileJsonRequest => this.tileJsonRequest.abort());
@ -131,7 +173,7 @@ class Map extends React.Component {
render() {
return (
<div style={{height: "100%"}}>
<div style={{height: "100%"}} className="map">
<ErrorMessage bind={[this, 'error']} />
<div
style={{height: "100%"}}

Wyświetl plik

@ -1,3 +1,13 @@
.map{
.leaflet-popup-content{
.title{
font-weight: bold;
margin-bottom: 8px;
}
.asset-links{
margin-top: 8px;
padding-left: 16px;
}
}
}

Wyświetl plik

@ -7,20 +7,22 @@ import MapView from './MapView';
import Console from './Console';
import $ from 'jquery';
$("[data-dashboard]").each(function(){
ReactDOM.render(<Dashboard/>, $(this).get(0));
});
$(function(){
$("[data-dashboard]").each(function(){
ReactDOM.render(<Dashboard/>, $(this).get(0));
});
$("[data-mapview]").each(function(){
let props = $(this).data();
delete(props.mapview);
ReactDOM.render(<MapView {...props}/>, $(this).get(0));
});
$("[data-mapview]").each(function(){
let props = $(this).data();
delete(props.mapview);
ReactDOM.render(<MapView {...props}/>, $(this).get(0));
});
$("[data-console]").each(function(){
ReactDOM.render(<Console
lang={$(this).data("console-lang")}
height={$(this).data("console-height")}
autoscroll={typeof $(this).attr("autoscroll") !== 'undefined' && $(this).attr("autoscroll") !== false}
>{$(this).text()}</Console>, $(this).get(0));
$("[data-console]").each(function(){
ReactDOM.render(<Console
lang={$(this).data("console-lang")}
height={$(this).data("console-height")}
autoscroll={typeof $(this).attr("autoscroll") !== 'undefined' && $(this).attr("autoscroll") !== false}
>{$(this).text()}</Console>, $(this).get(0));
});
});

Wyświetl plik

@ -0,0 +1,9 @@
.leaflet-container .leaflet-control-mouseposition {
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 0 0 5px #bbb;
padding: 0 5px;
margin:0;
color: #333;
font: 11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}

Wyświetl plik

@ -0,0 +1,48 @@
L.Control.MousePosition = L.Control.extend({
options: {
position: 'bottomleft',
separator: ' : ',
emptyString: 'Unavailable',
lngFirst: false,
numDigits: 5,
lngFormatter: undefined,
latFormatter: undefined,
prefix: ""
},
onAdd: function (map) {
this._container = L.DomUtil.create('div', 'leaflet-control-mouseposition');
L.DomEvent.disableClickPropagation(this._container);
map.on('mousemove', this._onMouseMove, this);
this._container.innerHTML=this.options.emptyString;
return this._container;
},
onRemove: function (map) {
map.off('mousemove', this._onMouseMove)
},
_onMouseMove: function (e) {
var lng = this.options.lngFormatter ? this.options.lngFormatter(e.latlng.lng) : L.Util.formatNum(e.latlng.lng, this.options.numDigits);
var lat = this.options.latFormatter ? this.options.latFormatter(e.latlng.lat) : L.Util.formatNum(e.latlng.lat, this.options.numDigits);
var value = this.options.lngFirst ? lng + this.options.separator + lat : lat + this.options.separator + lng;
var prefixAndValue = this.options.prefix + ' ' + value;
this._container.innerHTML = prefixAndValue;
}
});
L.Map.mergeOptions({
positionControl: false
});
L.Map.addInitHook(function () {
if (this.options.positionControl) {
this.positionControl = new L.Control.MousePosition();
this.addControl(this.positionControl);
}
});
L.control.mousePosition = function (options) {
return new L.Control.MousePosition(options);
};

Wyświetl plik

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Alex Ebadirad
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Wyświetl plik

@ -0,0 +1,96 @@
# Leaflet.AutoLayers
A dynamic leaflet layers control that pulls from multiple mapservers and manages basemaps and overlays plus their order.
## Getting Started
See [this demo page](http://aebadirad.github.io/Leaflet.AutoLayers/example/index.html) for a full example or [this barebones demonstration](http://aebadirad.github.io/Leaflet.AutoLayers/example/simple.html) of the simpliest way to configure the plugin.
New! WMS support! Huzzah! Splits the WMS layers up for you so that you can turn them off/on and declare basemaps, automatically pulls layers. [See this demo for an example](http://aebadirad.github.io/Leaflet.AutoLayers/example/wms.html).
### Configuration Breakdown
The configuration is an object that is passed in as the first signature on the method call (L.control.autolayers()). The second is the standard Layers options object which is optional.
List of possible configuration keys:
* overlays: OPTIONAL - standard built control layers object as built statically [here](http://leafletjs.com/examples/layers-control.html)
* baseLayers: OPTIONAL - standard built control layers object as built statically [here](http://leafletjs.com/examples/layers-control.html)
* selectedBasemap: RECOMMENDED - determines which baselayer gets selected first by layer 'name'
* selectedOverlays: OPTIONAL - determines which overlays are auto-selected on load
* mapServers: OPTIONAL - but this is kind of the whole point of this plugin
* url: REQUIRED - the base url of the service (e.g. http://services.arcgisonline.com/arcgis/rest/services)
* baseLayers: RECOMMENDED - tells the control what layers to place in base maps, else all from this server go into overlays
* dictionary: REQUIRED - where the published service list dictionary is (e.g. http://services.arcgisonline.com/arcgis/rest/services?f=pjson)
* tileUrl: REQUIRED - (EXCEPT WMS) - the part that comes after the layer name in the tileserver with xyz coords placeholders (e.g. /MapServer/tile/{z}/{y}/{x} or /{z}/{x}/{y}.png)
* name: REQUIRED - the name of the server, or however you want to identify the source
* type: REQUIRED - current options: esri or nrltileserver
* whitelist: OPTIONAL - ONLY display these layers, matches against both baselayers and overlays. Do not use with blacklist.
* blacklist: OPTIONAL - DO NOT display these layers, matches against both baselayers and overlays. Do not use with whitelist.
### Prerequisities
1. A recent browser (IE 10 or later, Firefox, Safari, Chrome etc)
2. [Leaflet](https://github.com/Leaflet/Leaflet) mapping library
That's it! It has its own built in ajax and comes bundled with x2js, you can drop both of these for your own with some slight modifications.
### Installing
1. Clone
2. Include leaflet-autolayers.js and the accompanying css/images in your project where appropriate
3. Create your configuration and place L.control.autolayers(config).addTo(map) where you have your map implemented
4. And that's it!
Sample Configuration that pulls from the public ArcGIS and Navy Research Labs tileservers:
```
var config = {
overlays: overlays, //custom overlays group that are static
baseLayers: baseLayers, //custom baselayers group that are static
selectedBasemap: 'Streets', //selected basemap when it loads
selectedOverlays: ["ASTER Digital Elevation Model 30M", "ASTER Digital Elevation Model Color 30M", "Cities"], //which overlays should be on by default
mapServers: [{
"url": "http://services.arcgisonline.com/arcgis/rest/services",
"dictionary": "http://services.arcgisonline.com/arcgis/rest/services?f=pjson",
"tileUrl": "/MapServer/tile/{z}/{y}/{x}",
"name": "ArcGIS Online",
"type": "esri",
"baseLayers": ["ESRI_Imagery_World_2D", "ESRI_StreetMap_World_2D", "NGS_Topo_US_2D"],
"whitelist": ["ESRI_Imagery_World_2D", "ESRI_StreetMap_World_2D", "NGS_Topo_US_2D"]
}, {
"url": "http://geoint.nrlssc.navy.mil/nrltileserver",
"dictionary": "http://geoint.nrlssc.navy.mil/nrltileserver/wms?REQUEST=GetCapabilities&VERSION=1.1.1&SERVICE=WMS",
"tileUrl": "/{z}/{x}/{y}.png",
"name": "Navy NRL",
"type": "nrltileserver",
"baseLayers": ["bluemarble", "Landsat7", "DTED0_GRID_COLOR1", "ETOPO1_COLOR1", "NAIP", "DRG_AUTO"],
"blacklist": ["BlackMarble"]
}]
};
```
## Deployment
Make sure all your layers you include are of the same projection. Currently map projection redrawing based on baselayer is not implemented, so if you don't have matching layer projections, things will not line up properly.
## Contributing
Contributions, especially for other map servers or enhancements welcome.
## Versioning
For now it's going to remain in beta until the Leaflet 1.0.0 release. After that time a standard version 1.x will begin.
## Authors
* **Alex Ebadirad** - *Initial work* - [aebadirad](https://github.com/aebadirad)
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
## Acknowledgments
* [Houston Engineering, INC](www.heigeo.com) for the simple ajax utility
* [x2js](https://github.com/abdmob/x2js) for parsing the WMS response to json

Wyświetl plik

@ -0,0 +1,182 @@
.leaflet-control-autolayers-title {
cursor: pointer;
}
.leaflet-control-autolayers-close {
display: inline-block;
background-image: url(../images/close.png);
height: 18px;
width: 18px;
margin-right: 0;
float: right;
vertical-align: middle;
text-align: right;
margin-top: 1px;
}
.leaflet-control-autolayers-title {
display: inline-block !important;
width: 90%;
height: 20px;
margin: 0;
padding: 0;
font-weight: bold;
font-size: 1.2em;
}
.leaflet-control-layers-base {
padding-bottom: 1px;
width: 340px;
overflow-y: scroll;
overflow-x: hidden;
height: 220px;
display: none;
}
.leaflet-control-layers-base label {
height: 24px;
margin: 0;
min-width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.leaflet-control-layers-base label:hover {
background-color: #CCC;
}
.leaflet-control-layers-overlays {
padding-bottom: 1px;
padding-top: 6px;
margin: 0;
width: 340px;
overflow-y: scroll;
overflow-x: hidden;
height: 220px;
display: block;
}
.leaflet-control-layers-overlays label {
height: 24px;
min-width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.leaflet-control-layers-overlays label:hover {
background-color: #CCC;
}
.leaflet-control-layers-selected {
padding-bottom: 1px;
width: 340px;
overflow-y: scroll;
overflow-x: hidden;
height: 150px;
display: none;
}
.selected-label {
height: 21px;
width: 330px;
white-space: nowrap;
overflow: hidden;
text-align: left;
}
.selected-label:hover {
background-color: #CCC;
}
.selected-name {
display: inline-block;
width: 270px;
text-overflow: ellipsis;
overflow: hidden;
}
.selected-remove {
display: inline-block;
background-image: url(../images/remove.png);
height: 14px;
width: 14px;
margin-right: 4px;
margin-bottom: 4px;
cursor: pointer;
}
.selected-none {
display: inline-block;
height: 11px;
width: 16px;
}
.selected-up {
display: inline-block;
background-image: url(../images/arrow-up.png);
background-repeat: no-repeat;
background-position: top;
height: 12px;
width: 16px;
cursor: pointer;
}
.selected-down {
display: inline-block;
background-image: url(../images/arrow-down.png);
background-repeat: no-repeat;
background-position: top;
height: 12px;
width: 16px;
cursor: pointer;
}
.leaflet-control-attribution {
height: 16px;
overflow: hidden;
text-align: left;
transition: height 0.5s;
/* Animation time */
-webkit-transition: height 0.5s;
/* For Safari */
}
.leaflet-control-attribution:hover {
height: 150px;
}
.map-filter {
display: none;
margin-top: 6px;
}
.map-filter-box-base {
width: 75%;
margin-bottom: 3px;
height: 20px;
padding: 0;
text-align: left;
}
.map-filter-box-overlays {
width: 75%;
margin-bottom: 3px;
height: 20px;
padding: 0;
text-align: left;
display: none;
}
.leaflet-control-layers-item-container{
padding-top: 2px;
padding-bottom: 4px;
}
.leaflet-control-layers-item-container:hover{
background: #eee;
cursor: pointer;
}

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 336 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 329 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 202 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.8 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.5 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.9 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 797 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 148 B

Wyświetl plik

@ -25,6 +25,8 @@
<link rel="stylesheet" type="text/css" href="{% static 'app/bundles/css/main.css' %}" />
<script src="{% static 'app/js/vendor/modernizr-2.8.3.min.js' %}"></script>
<script src="{% static 'app/js/vendor/jquery-1.11.2.min.js' %}"></script>
{% load render_bundle from webpack_loader %}
{% render_bundle 'main' %}
<title>{{title|default:"Login"}} - WebODM</title>
</head>
<body data-admin-utc-offset="{% now "Z" %}">
@ -109,4 +111,5 @@ $(function(){
});
</script>
<script src="{% static 'app/js/vendor/bootstrap.min.js' %}"></script>
</html>

Wyświetl plik

@ -1,5 +1,6 @@
{% extends "app/base.html" %}
{% load i18n static %}
<!--{% if request.user.is_authenticated %}
{% if user.is_superuser %}
<li>
@ -293,4 +294,4 @@
</div>
</section>
</div>
{% endblock %}
{% endblock %}

Wyświetl plik

@ -1,6 +1,5 @@
{% extends "app/logged_in_base.html" %}
{% load i18n %}
{% load render_bundle from webpack_loader %}
{% block content %}
<h3>{{title}}</h3>
@ -10,6 +9,5 @@
data-{{key}}="{{value}}"
{% endfor %}
></div>
{% render_bundle 'main' %}
{% endblock %}

Wyświetl plik

@ -1,6 +1,5 @@
{% extends "app/logged_in_base.html" %}
{% load i18n tz %}
{% load render_bundle from webpack_loader %}
{% block content %}
<h3>Processing Node</h3>
@ -37,6 +36,4 @@
</div>
{% endif %}
{% render_bundle 'main' %}
{% endblock %}

Wyświetl plik

@ -1,3 +1,5 @@
from guardian.shortcuts import assign_perm
from app import pending_actions
from .classes import BootTestCase
from rest_framework.test import APIClient
@ -206,6 +208,11 @@ class TestApi(BootTestCase):
port=999
)
another_pnode = ProcessingNode.objects.create(
hostname="localhost",
port=998
)
# Cannot list processing nodes as guest
res = client.get('/api/processingnodes/')
self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
@ -215,7 +222,19 @@ class TestApi(BootTestCase):
client.login(username="testuser", password="test1234")
# Can list processing nodes as normal user
# Cannot list processing nodes, unless permissions have been granted
res = client.get('/api/processingnodes/')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertTrue(len(res.data) == 0)
user = User.objects.get(username="testuser")
self.assertFalse(user.is_staff)
self.assertFalse(user.is_superuser)
self.assertFalse(user.has_perm('view_processingnode', pnode))
assign_perm('view_processingnode', user, pnode)
self.assertTrue(user.has_perm('view_processingnode', pnode))
# Now we can list processing nodes as normal user
res = client.get('/api/processingnodes/')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertTrue(len(res.data) == 1)
@ -226,6 +245,10 @@ class TestApi(BootTestCase):
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertTrue(len(res.data) == 1)
res = client.get('/api/processingnodes/?id={}'.format(another_pnode.id))
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertTrue(len(res.data) == 0)
# Can filter nodes with valid options
res = client.get('/api/processingnodes/?has_available_options=true')
self.assertEqual(res.status_code, status.HTTP_200_OK)
@ -264,6 +287,6 @@ class TestApi(BootTestCase):
# Verify node has been created
res = client.get('/api/processingnodes/')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertTrue(len(res.data) == 1)
self.assertTrue(res.data[0]["port"] == 1000)
self.assertTrue(len(res.data) == 2)
self.assertTrue(res.data[1]["port"] == 1000)

Wyświetl plik

@ -150,3 +150,5 @@ class TestApp(BootTestCase):
self.assertTrue(scheduler.update_nodes_info(background=True).join() is None)
self.assertTrue(scheduler.teardown() is None)

Wyświetl plik

@ -38,7 +38,7 @@ def map(request, project_pk=None, task_pk=None):
raise Http404()
if task_pk is not None:
tassek = get_object_or_404(Task.objects.defer('orthophoto'), pk=task_pk, project=project)
task = get_object_or_404(Task.objects.defer('orthophoto'), pk=task_pk, project=project)
title = task.name
tiles = [task.get_tiles_json_data()]
else:

Wyświetl plik

@ -1,8 +1,9 @@
from django.contrib import admin
from guardian.admin import GuardedModelAdmin
from .models import ProcessingNode
class ProcessingNodeAdmin(admin.ModelAdmin):
class ProcessingNodeAdmin(GuardedModelAdmin):
fields = ('hostname', 'port')
admin.site.register(ProcessingNode, ProcessingNodeAdmin)

Wyświetl plik

@ -4,6 +4,9 @@ from django.db import models
from django.contrib.postgres import fields
from django.utils import timezone
from django.dispatch import receiver
from guardian.models import GroupObjectPermissionBase
from guardian.models import UserObjectPermissionBase
from .api_client import ApiClient
import json
from django.db.models import signals
@ -169,8 +172,21 @@ class ProcessingNode(models.Model):
else:
raise ProcessingException("Unknown response: {}".format(result))
class Meta:
permissions = (
('view_processingnode', 'Can view processing node'),
)
# First time a processing node is created, automatically try to update
@receiver(signals.post_save, sender=ProcessingNode, dispatch_uid="update_processing_node_info")
def auto_update_node_info(sender, instance, created, **kwargs):
if created:
instance.update_node_info()
class ProcessingNodeUserObjectPermission(UserObjectPermissionBase):
content_object = models.ForeignKey(ProcessingNode)
class ProcessingNodeGroupObjectPermission(GroupObjectPermissionBase):
content_object = models.ForeignKey(ProcessingNode)

Wyświetl plik

@ -30,7 +30,7 @@
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"leaflet": "^1.0.1",
"leaflet-basemaps": "^0.1.1",
"leaflet-measure": "^2.0.5",
"node-sass": "^3.10.1",
"object.values": "^1.0.3",
"react": "^15.3.2",

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.1 MiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1009 KiB