Added layer list on map display, popup info, download links on map, measurement tool
|
@ -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
|
||||
|
|
|
@ -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/<projectId>/tasks : list all tasks belonging to a project<br/>
|
||||
- /api/projects/<projectId>/tasks/<taskId> : get task details
|
||||
access to view, add, change or delete them.
|
||||
"""
|
||||
filter_fields = ('id', 'name', 'description', 'created_at')
|
||||
serializer_class = ProjectSerializer
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>);
|
||||
|
|
|
@ -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;
|
||||
|
|
@ -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>);
|
||||
}
|
||||
|
|
|
@ -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: © 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 © Esri — 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: '© <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%"}}
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
.map{
|
||||
.leaflet-popup-content{
|
||||
.title{
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.asset-links{
|
||||
margin-top: 8px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
|
@ -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.
|
|
@ -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
|
|
@ -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;
|
||||
}
|
Po Szerokość: | Wysokość: | Rozmiar: 336 B |
Po Szerokość: | Wysokość: | Rozmiar: 329 B |
Po Szerokość: | Wysokość: | Rozmiar: 202 B |
Po Szerokość: | Wysokość: | Rozmiar: 2.8 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.5 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 3.9 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.7 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 797 B |
Po Szerokość: | Wysokość: | Rozmiar: 148 B |
|
@ -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>
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -150,3 +150,5 @@ class TestApp(BootTestCase):
|
|||
self.assertTrue(scheduler.update_nodes_info(background=True).join() is None)
|
||||
|
||||
self.assertTrue(scheduler.teardown() is None)
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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",
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 1.1 MiB Po Szerokość: | Wysokość: | Rozmiar: 1009 KiB |