diff --git a/app/api/processingnodes.py b/app/api/processingnodes.py index 250583b3..8ec57978 100644 --- a/app/api/processingnodes.py +++ b/app/api/processingnodes.py @@ -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 diff --git a/app/api/projects.py b/app/api/projects.py index bdd13d2d..1ec04be2 100644 --- a/app/api/projects.py +++ b/app/api/projects.py @@ -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.

- - /api/projects/<projectId>/tasks : list all tasks belonging to a project
- - /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 diff --git a/app/api/tasks.py b/app/api/tasks.py index e4db773b..6689a673 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -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())) diff --git a/app/boot.py b/app/boot.py index 5c765265..4564d347 100644 --- a/app/boot.py +++ b/app/boot.py @@ -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: diff --git a/app/fixtures/orthophoto.tif b/app/fixtures/orthophoto.tif index 1e53e8be..1c66fe9c 100644 Binary files a/app/fixtures/orthophoto.tif and b/app/fixtures/orthophoto.tif differ diff --git a/app/static/app/js/MapView.jsx b/app/static/app/js/MapView.jsx index 23e8c8e5..ba23d869 100644 --- a/app/static/app/js/MapView.jsx +++ b/app/static/app/js/MapView.jsx @@ -35,7 +35,7 @@ class MapView extends React.Component {
- Orthophoto opacity: + Orthophotos opacity:
); diff --git a/app/static/app/js/classes/AssetDownloads.js b/app/static/app/js/classes/AssetDownloads.js new file mode 100644 index 00000000..60c7afde --- /dev/null +++ b/app/static/app/js/classes/AssetDownloads.js @@ -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; + diff --git a/app/static/app/js/components/AssetDownloadButtons.jsx b/app/static/app/js/components/AssetDownloadButtons.jsx index d8580224..84552c67 100644 --- a/app/static/app/js/components/AssetDownloadButtons.jsx +++ b/app/static/app/js/components/AssetDownloadButtons.jsx @@ -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 (
); } diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index a8c032ac..7fa0fbd8 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -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: '© OpenStreetMap', 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(`
${info.name}
+
Bounds: [${layer.options.bounds.toBBoxString().split(",").join(", ")}]
+ + `); // 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 ( -
+
, $(this).get(0)); -}); +$(function(){ + $("[data-dashboard]").each(function(){ + ReactDOM.render(, $(this).get(0)); + }); -$("[data-mapview]").each(function(){ - let props = $(this).data(); - delete(props.mapview); - ReactDOM.render(, $(this).get(0)); -}); + $("[data-mapview]").each(function(){ + let props = $(this).data(); + delete(props.mapview); + ReactDOM.render(, $(this).get(0)); + }); -$("[data-console]").each(function(){ - ReactDOM.render({$(this).text()}, $(this).get(0)); + $("[data-console]").each(function(){ + ReactDOM.render({$(this).text()}, $(this).get(0)); + }); }); diff --git a/app/static/app/js/vendor/leaflet/L.Control.MousePosition.css b/app/static/app/js/vendor/leaflet/L.Control.MousePosition.css new file mode 100644 index 00000000..71e09a23 --- /dev/null +++ b/app/static/app/js/vendor/leaflet/L.Control.MousePosition.css @@ -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; +} + diff --git a/app/static/app/js/vendor/leaflet/L.Control.MousePosition.js b/app/static/app/js/vendor/leaflet/L.Control.MousePosition.js new file mode 100644 index 00000000..656bc78b --- /dev/null +++ b/app/static/app/js/vendor/leaflet/L.Control.MousePosition.js @@ -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); +}; diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/LICENSE b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/LICENSE new file mode 100644 index 00000000..13093b01 --- /dev/null +++ b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/LICENSE @@ -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. diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/README.md b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/README.md new file mode 100644 index 00000000..bd5a253a --- /dev/null +++ b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/README.md @@ -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 diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/css/leaflet.auto-layers.css b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/css/leaflet.auto-layers.css new file mode 100644 index 00000000..fd3a9a55 --- /dev/null +++ b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/css/leaflet.auto-layers.css @@ -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; +} \ No newline at end of file diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/arrow-down.png b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/arrow-down.png new file mode 100644 index 00000000..bcef3ff4 Binary files /dev/null and b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/arrow-down.png differ diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/arrow-up.png b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/arrow-up.png new file mode 100644 index 00000000..5a396c9e Binary files /dev/null and b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/arrow-up.png differ diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/close.png b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/close.png new file mode 100644 index 00000000..4ba0bd49 Binary files /dev/null and b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/close.png differ diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/layers-2x.png b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/layers-2x.png new file mode 100644 index 00000000..a2cf7f9e Binary files /dev/null and b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/layers-2x.png differ diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/layers.png b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/layers.png new file mode 100644 index 00000000..bca0a0e4 Binary files /dev/null and b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/layers.png differ diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/marker-icon-2x.png b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/marker-icon-2x.png new file mode 100644 index 00000000..0015b649 Binary files /dev/null and b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/marker-icon-2x.png differ diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/marker-icon.png b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/marker-icon.png new file mode 100644 index 00000000..e2e9f757 Binary files /dev/null and b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/marker-icon.png differ diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/marker-shadow.png b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/marker-shadow.png new file mode 100644 index 00000000..d1e773c7 Binary files /dev/null and b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/marker-shadow.png differ diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/remove.png b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/remove.png new file mode 100644 index 00000000..f2b54600 Binary files /dev/null and b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/images/remove.png differ diff --git a/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers.js b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers.js new file mode 100644 index 00000000..f7e91dc5 --- /dev/null +++ b/app/static/app/js/vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers.js @@ -0,0 +1,1478 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016 Alex Ebadirad, https://github.com/aebadirad/Leaflet.AutoLayers + +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. +*/ + +//we extend the control object to include the new AutoLayers object + +L.Control.AutoLayers = L.Control.extend({ + options: { + collapsed: true, + position: 'topright', + autoZIndex: true + }, + mapConfig: {}, + mapLayers: [], + overLays: [], + baseMaps: [], + selectedOverlays: [], + zIndexBase: 1, + selectedBasemap: null, + layersToAdd: {}, + + countZIndexBase: function(layers) { + for (var i = 0; i < layers.length; i++) { + var layer = layers[i]; + if (!layer.baseLayer) { + autoControl.zIndexBase++; + } + } + }, + + initialize: function(mapConfig, options) { + L.setOptions(this, options); + this.mapConfig = mapConfig; + this._layers = {}; + this._lastZIndex = 0; + this._handlingClick = false; + this._initLayout(); + var baseLayers = mapConfig.baseLayers; + var overlays = mapConfig.overlays; + var selectedBasemap = this.selectedBasemap = mapConfig.selectedBasemap; + + for (var i in baseLayers) { + this._addLayer(baseLayers[i], i); + } + + for (var i in overlays) { + this._addLayer(overlays[i], i, true); + this.overLays[i] = overlays[i]; + } + + //incase we have no mapservers but still wish to add custom overlays pre-selected + this._selectOverlays(); + + this.fetchMapData(); + }, + + onAdd: function(map) { + this._initEvents(); + this._update(); + map.on('layeradd', this._onLayerChange, this).on('layerremove', this._onLayerChange, this); + return this._container; + }, + + onRemove: function(map) { + map + .off('layeradd', this._onLayerChange, this) + .off('layerremove', this._onLayerChange, this); + }, + + addBaseLayer: function(layer, name) { + this._addLayer(layer, name); + this._update(); + return this; + }, + + addOverlay: function(layer, name) { + this._addLayer(layer, name, true); + this._update(); + return this; + }, + + removeLayer: function(layer) { + var id = L.stamp(layer); + delete this._layers[id]; + this._update(); + return this; + }, + + _initLayout: function() { + var className = 'leaflet-control-layers', + container = this._container = L.DomUtil.create('div', className); + + //Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released + container.setAttribute('aria-haspopup', true); + + if (!L.Browser.touch) { + L.DomEvent + .disableClickPropagation(container) + .disableScrollPropagation(container); + } else { + L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation); + } + + var form = this._form = L.DomUtil.create('form', className + '-list'); + + if (this.options.collapsed) { + + var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container); + link.href = '#'; + link.title = 'Layers'; + + if (L.Browser.touch) { + L.DomEvent + .on(link, 'click', L.DomEvent.stop) + .on(link, 'click', this._expand, this); + } else { + L.DomEvent.on(link, 'focus', this._expand, this); + } + //Work around for Firefox android issue https://github.com/Leaflet/Leaflet/issues/2033 + // L.DomEvent.on(form, 'click', function() { + // setTimeout(L.bind(this._onInputClick, this), 0); + // }, this); + + // TODO keyboard accessibility + } else { + this._expand(); + } + + //base layers are made here + var baseLayersDiv = this._baseLayersDiv = L.DomUtil.create('div', 'leaflet-control-layers-tab', + form); + this._baseLayersTitle = L.DomUtil.create('div', 'leaflet-control-autolayers-title', + baseLayersDiv); + this._baseLayersTitle.innerHTML = 'Base Maps'; + this._baseLayersClose = L.DomUtil.create('span', 'leaflet-control-autolayers-close', + baseLayersDiv); + var baseLayersBox = this._baseLayersBox = L.DomUtil.create('div', 'map-filter', baseLayersDiv); + var baseLayersFilter = this._baseLayersFilter = L.DomUtil.create('input', + 'map-filter-box-base', baseLayersBox); + baseLayersFilter.setAttribute('placeholder', 'Filter Base Layer List'); + baseLayersFilter.setAttribute('autocomplete', 'off'); + this._baseLayersList = L.DomUtil.create('div', className + '-base', baseLayersDiv); + this._separator = L.DomUtil.create('div', className + '-separator', form); + + //overlays are done here + var overlaysLayersDiv = this._overlaysDiv = L.DomUtil.create('div', + 'leaflet-control-layers-tab', form); + this._overlaysLayersTitle = L.DomUtil.create('div', 'leaflet-control-autolayers-title', + overlaysLayersDiv); + this._overlaysLayersTitle.innerHTML = 'Orthophotos'; + var overlaysLayersBox = this._overlaysLayersBox = L.DomUtil.create('div', 'map-filter', + overlaysLayersDiv); + var overlaysLayersFilter = this._overlaysLayersFilter = L.DomUtil.create('input', + 'map-filter-box-overlays', overlaysLayersBox); + overlaysLayersFilter.setAttribute('placeholder', 'Filter Overlays List'); + overlaysLayersFilter.setAttribute('autocomplete', 'off'); + this._overlaysList = L.DomUtil.create('div', className + '-overlays', overlaysLayersDiv); + + container.appendChild(form); + + //check to see if we have any preadded + + return true; + }, + + _initEvents: function() { + var self = this; + var overlaysFilterBox = this._overlaysLayersFilter; + var baseFilterBox = this._baseLayersFilter; + + //stop the traditional submit from occurring + L.DomEvent.addListener(overlaysFilterBox, 'submit', function(e) { + L.DomEvent.stopPropagation(e); + }); + + L.DomEvent.addListener(baseFilterBox, 'submit', function(e) { + L.DomEvent.stopPropagation(e); + }); + + //now we bind the filtering to each box + L.DomEvent.addListener(overlaysFilterBox, 'keyup', function(e) { + var filterBoxValue = this.value.toLowerCase(); + var displayLayers = this.parentNode.parentNode.getElementsByClassName( + 'leaflet-control-layers-overlays')[0].children; + if (filterBoxValue.length > 2) { + for (var i = 0; i < displayLayers.length; i++) { + if (displayLayers[i].innerText.toLowerCase().indexOf( + filterBoxValue) > -1) { + displayLayers[i].style.display = 'block'; + } else { + displayLayers[i].style.display = 'none'; + } + } + } else { + for (var i = 0; i < displayLayers.length; i++) { + displayLayers[i].style.display = 'block'; + } + } + }); + + //now the baselayers filter box + L.DomEvent.addListener(baseFilterBox, 'keyup', function(e) { + var filterBoxValue = this.value.toLowerCase(); + var displayLayers = this.parentNode.parentNode.getElementsByClassName( + 'leaflet-control-layers-base')[0].children; + if (filterBoxValue.length > 2) { + for (var i = 0; i < displayLayers.length; i++) { + if (displayLayers[i].innerText.toLowerCase().indexOf( + filterBoxValue) > -1) { + displayLayers[i].style.display = 'block'; + } else { + displayLayers[i].style.display = 'none'; + } + } + } else { + for (var i = 0; i < displayLayers.length; i++) { + displayLayers[i].style.display = 'block'; + } + } + }); + + //open and close setup + var titles = this._form.getElementsByClassName('leaflet-control-autolayers-title'); + for (var t = 0; t < titles.length; t++) { + L.DomEvent.addListener(titles[t], 'click', function(e) { + var overlayOrBase; + if (e.currentTarget.innerText === 'Overlays') { + overlayOrBase = 'overlays'; + } + if (e.currentTarget.innerText === 'Base Maps') { + overlayOrBase = 'base'; + } + + var allTabs = this.parentNode.parentNode.getElementsByClassName( + 'leaflet-control-layers-tab'); + for (var i = 0; i < allTabs.length; i++) { + var tab = allTabs[i].getElementsByTagName('div'); + + for (var m = 0; m < tab.length; m++) { + var tabContent = tab[m]; + if (tabContent.className === "leaflet-control-layers-item-container") continue; + + if (tabContent.className !== 'leaflet-control-autolayers-title') { + tabContent.style.display = 'none'; + + } + } + } + + var thisTab = this.parentNode.children; + for (var i = 0; i < thisTab.length; i++) { + thisTab[i].style.display = 'block'; + var filter = thisTab[i].getElementsByClassName('map-filter-box-' + overlayOrBase); + if (filter.length > 0) { + filter[0].style.display = 'block'; + } + } + + if (e.currentTarget.innerText === 'Overlays' || e.currentTarget + .innerText === 'Base Maps') { + var filterBoxValue = this.parentNode.getElementsByClassName('map-filter')[0].children[0].value + .toLowerCase(); + var displayLayers = this.parentNode.getElementsByClassName('leaflet-control-layers-' + + overlayOrBase)[0].getElementsByTagName('label'); + if (filterBoxValue.length > 2) { + for (var i = 0; i < displayLayers.length; i++) { + if (displayLayers[i].innerText.toLowerCase().indexOf( + filterBoxValue) > -1) { + displayLayers[i].style.display = 'block'; + } else { + displayLayers[i].style.display = 'none'; + } + } + } + } else { + // for (var i = 0; i < displayLayers.length; i++) { + // displayLayers[i].style.display = 'block'; + // } + } + }); + } + + //x in the corner to close + var closeControl = this._baseLayersClose; + L.DomEvent.addListener(closeControl, 'click', function(e) { + this.parentNode.parentNode.parentNode.className = this.parentNode.parentNode.parentNode.className + .replace( + 'leaflet-control-layers-expanded', ''); + }); + + //fix pesky zooming, have to dynamically measure the hidden div too! Make sure you do that! + var overlayBox = this._overlaysList; + L.DomEvent.addListener(overlayBox, 'mousewheel', function(e) { + var delta = e.wheelDelta || -e.detail; + this.scrollTop += (delta < 0 ? 1 : -1) * 30; + e.preventDefault(); + }); + var baseBox = this._baseLayersList; + L.DomEvent.addListener(baseBox, 'mousewheel', function(e) { + var delta = e.wheelDelta || -e.detail; + this.scrollTop += (delta < 0 ? 1 : -1) * 30; + e.preventDefault(); + }); + + //make sure we collapse the control on mapclick + this._map.on('click', this._collapse, this); + + }, + + _initMaps: function(mapLayers) { + var mapConfig = this.mapConfig; + var self = this; + var selected; + for (var i = 0; i < mapLayers.length; i++) { + var mapLayer = mapLayers[i]; + if (!mapLayer.baseLayer) { + self.zIndexBase++; + } + //set some default layer options + var layerOpts = { + noWrap: mapConfig.noWrap === false ? mapConfig.noWrap : true, + continuousWorld: mapConfig.continuousWorld === true ? mapConfig.continuousWorld : false, + tileSize: mapConfig.tileSize ? mapConfig.tileSize : 256, + tms: mapLayer.tms ? mapLayer.tms : false, + zoomOffset: mapLayer.zoomOffset ? mapLayer.zoomOffset : 0, + minZoom: 0, + maxZoom: mapConfig.maxZoom ? mapConfig.maxZoom : 15, + attribution: mapLayer.attribution ? mapLayer.attribution : 'Source Currently Unknown' + }; + var layer; + if (mapLayer.type === 'wms') { + layer = L.tileLayer.wms(mapLayer.url, { + layers: mapLayer.name, + format: 'image/png', + maxZoom: 16, + transparent: true + }); + mapLayer.name = mapLayer.title; + //self.baseMaps[mapLayer.name] = layer; + } else { + layer = new L.tileLayer(mapLayer.url, layerOpts); + } + if (mapLayer.baseLayer) { + self.baseMaps[String(mapLayer.name).trim()] = layer; + self._addLayer(layer, mapLayer.name); + + } else { + self.overLays[String(mapLayer.name).trim()] = layer; + self._addLayer(layer, mapLayer.name, true); + } + if (mapLayer.baseLayer && !self.selectedBasemap) { + self.selectedBasemap = mapLayer.name.trim(); + layer.addTo(map); + } else if (mapLayer.name === self.selectedBasemap && mapLayer.baseLayer) { + self.selectedBasemap = mapLayer.name.trim(); + layer.addTo(map); + } + } + this._selectOverlays(); + }, + + fetchMapData: function() { + var mapServers = this.mapConfig.mapServers; + var self = this; + var mapLayers = []; + if (mapServers) { + for (var i = 0; i < mapServers.length; i++) { + var layers = this.fetchMapDictionary(mapServers[i]); + } + } + }, + + fetchMapDictionary: function(mapServer) { + var self = this; + var layers = []; + this.layersToAdd[mapServer.name] = []; + var url = mapServer.dictionary.replace(/&/g, '&'); + ajax(url, function(res) { + + if (mapServer.type === 'esri') { + var response = JSON.parse(res); + var layersToAdd = self._parseESRILayers(mapServer, response); + if (layersToAdd && layersToAdd.length > 0) { + layers = layersToAdd; + } + + } else if (mapServer.type === 'nrltileserver') { + var x2js = new X2JS(); + var parser = new DOMParser(); + var xmlDoc = parser.parseFromString(res, "text/xml"); + var parsedRes = x2js.xml2json(xmlDoc); + var capability = parsedRes.WMT_MS_Capabilities.Capability.Layer; + var capLayers = capability.Layer; + var contactInfo = parsedRes.WMT_MS_Capabilities.Service.ContactInformation; + var crs = parseInt(capability.SRS.substring(5)); + var layersToAdd = self._parseNRLLayers(mapServer, capLayers, contactInfo, crs); + if (layersToAdd && layersToAdd.length > 0) { + layers = layersToAdd; + } + + } else if (mapServer.type === 'wms') { + var x2js = new X2JS(); + var parser = new DOMParser(); + var xmlDoc = parser.parseFromString(res, "text/xml"); + var parsedRes = x2js.xml2json(xmlDoc); + var capability = parsedRes.WMS_Capabilities.Capability.Layer; + var capLayers = capability.Layer; + var contactInfo = parsedRes.WMS_Capabilities.Service.ContactInformation; + var crs = capability.CRS[0]; + var layersToAdd = self._parseWMSLayers(mapServer, capLayers, contactInfo, crs); + if (layersToAdd && layersToAdd.length > 0) { + layers = layersToAdd; + } + } + //let's filter this list of any nulls (blacklists) + layers = layers.filter(function(l) { + return (l !== undefined && l !== null); + }); + self.mapLayers.push(layers); + self._initMaps(layers); + }); + }, + + _parseESRILayers: function(mapServer, response) { + var self = this; + //check to see if we have any root folder layers + var services = response.services; + var folders = []; + var layers = []; + var layersToAdd = []; + //check to see if any folders, if none it'll be empty array + folders = response.folders; + for (var i = 0; i < services.length; i++) { + if (services[i].type === 'MapServer') { + layersToAdd.push(self._parseESRILayer(mapServer, services[i])); + } + } + //now we check folders, why? we don't want calls within calls + if (folders.length > 0) { + layersToAdd.concat(self._parseESRIFolders(mapServer, folders)); + } + return layersToAdd; + }, + + _parseESRIFolders: function(mapServer, folders) { + var self = this; + var layersToAdd = []; + for (var f = 0; f < folders.length; f++) { + var folderUrl = mapServer.url + '/' + folders[f] + + '?f=pjson'; + ajax(folderUrl, function(res) { + var response = JSON.parse(res); + //check to see if we have any layers here + var services = response.services; + for (var i = 0; i < services.length; i++) { + if (services[i].type === 'MapServer') { + layersToAdd.push(self._parseESRILayer(mapServer, services[i], true)); + } + } + }); + } + return layersToAdd; + + }, + _parseESRILayer: function(mapServer, layer, folder = false) { + var layerToAdd = []; + if (folder) { + var fullName = layer.name.split('/'); + var layerName = fullName[1]; + var folderName = fullName[0]; + var layerUrl = mapServer.url + '/' + folderName + + '/' + layerName + mapServer.tileUrl; + var url = mapServer.url + '/' + folderName + + '/' + layerName + '/MapServer?f=pjson'; + + } else { + var layerName = layer.name; + var layerUrl = mapServer.url + '/' + layerName + + mapServer.tileUrl; + var url = mapServer.url + '/' + layerName + + '/MapServer?f=pjson'; + } + var layerObj = { + detailsUrl: url, + url: layerUrl, + name: layerName, + type: 'esri', + attribution: mapServer.name + ' - ' + layerName + }; + layerToAdd = this._createLayer(mapServer, layerObj, layerName); + return layerToAdd; + + }, + _parseNRLLayers: function(mapServer, layers, contactInfo, crs) { + var self = this; + for (var j = 0; j < layers.length; j++) { + var layer = layers[j]; + if (layer.Layer && layer.Layer.length > 1) { + self.layersToAdd[mapServer.name].concat(self._parseNRLLayers(mapServer, layer.Layer, + contactInfo, crs)); + } else { + self.layersToAdd[mapServer.name].push(self._parseNRLLayer(mapServer, layer, contactInfo, crs)); + } + } + return this.layersToAdd[mapServer.name]; + }, + _parseNRLLayer: function(mapServer, layer, contactInfo, crs) { + var layerToAdd = []; + var layerObj = { + crs: crs, + maxZoom: 18, + name: layer.Title, + type: 'nrl', + zoomOffset: 2, + tms: false, + noWrap: false, + continuousWorld: true, + attribution: mapServer.name + ': ' + contactInfo.ContactPersonPrimary + .ContactOrganization + ' - ' + layer.Title, + url: mapServer.url + '/openlayers/' + layer.Name + + mapServer.tileUrl + }; + + layerToAdd = this._createLayer(mapServer, layerObj, layer.Name); + return layerToAdd; + + }, + _parseWMSLayers: function(mapServer, layers, contactInfo, crs) { + var self = this; + for (var j = 0; j < layers.length; j++) { + var layer = layers[j]; + if (layer.Layer && layer.Layer.length > 1) { + self.layersToAdd[mapServer.name].concat(self._parseWMSLayers(mapServer, layer.Layer, + contactInfo, crs)); + } else { + self.layersToAdd[mapServer.name].push(self._parseWMSLayer(mapServer, layer, contactInfo, crs)); + } + } + return this.layersToAdd[mapServer.name]; + }, + + _parseWMSLayer: function(mapServer, layer, contactInfo, crs) { + var layerToAdd = []; + var layerObj = { + crs: crs, + maxZoom: 18, + name: layer.Name, + type: 'wms', + zoomOffset: 1, + tms: false, + noWrap: false, + continuousWorld: true, + title: layer.Title, + attribution: mapServer.name + ': ' + contactInfo.ContactPersonPrimary + .ContactOrganization + ' - ' + layer.Title, + url: mapServer.url + }; + layerToAdd = this._createLayer(mapServer, layerObj, layer.Title); + return layerToAdd; + + }, + + _createLayer: function(mapServer, layer, name) { + var blacklist = mapServer.blacklist; + var whitelist = mapServer.whitelist; + var layerToAdd = null; + var mapPass = -1; + if (mapServer.baseLayers) { + mapPass = mapServer.baseLayers.indexOf(name); + } + if ((whitelist && whitelist.indexOf(name) > -1) || (blacklist && blacklist.indexOf( + name) === -1) || (!blacklist && !whitelist)) { + if (!(mapServer.baseLayers) || mapPass > -1) { + layer.baseLayer = true; + } else { + layer.baseLayer = false; + } + layerToAdd = layer; + } + return layerToAdd; + }, + + _getLayerByName: function(name) { + var response; + for (var key in this._layers) { + var layer = this._layers[key]; + var layerName = layer.name; + if (layerName == name) { + response = layer; + break; + } + } + return response; + }, + + _addSelectedOverlay: function(name) { + if (this.selectedOverlays.indexOf(name) === -1) { + this.selectedOverlays.unshift(name); + this._buildSelectedOverlays(); + } + }, + + _buildSelectedOverlays: function() { + return; + var self = this; + var selectedOverlays = this.selectedOverlays; + var container = this._selectedList; + container.innerHTML = ''; + for (var i = 0; i < selectedOverlays.length; i++) { + var name = selectedOverlays[i]; + var selectedLabel = L.DomUtil.create('label', 'selected-label'); + var selectedRemove = L.DomUtil.create('span', 'selected-remove', selectedLabel); + var selectedName = L.DomUtil.create('span', 'selected-name', selectedLabel); + selectedName.innerHTML = name; + var selectedUp; + var selectedDown; + if (selectedOverlays.length === 1) { + selectedUp = L.DomUtil.create('span', 'selected-none', selectedLabel); + selectedDown = L.DomUtil.create('span', 'selected-none', selectedLabel); + } else { + if (selectedOverlays.length === (i + 1)) { + selectedUp = L.DomUtil.create('span', 'selected-up', selectedLabel); + selectedDown = L.DomUtil.create('span', 'selected-none', selectedLabel); + } else if (i === 0) { + selectedUp = L.DomUtil.create('span', 'selected-none', selectedLabel); + selectedDown = L.DomUtil.create('span', 'selected-down', selectedLabel); + } else { + selectedUp = L.DomUtil.create('span', 'selected-up', selectedLabel); + selectedDown = L.DomUtil.create('span', 'selected-down', selectedLabel); + } + } + + container.appendChild(selectedLabel); + + L.DomEvent.addListener(selectedRemove, 'click', function(e) { + var name = this.parentNode.getElementsByClassName('selected-name')[0].innerHTML; + var layer = self._getLayerByName(name); + self._map.removeLayer(layer.layer); + }); + //Now setup for up and down ordering + L.DomEvent.addListener(selectedUp, 'click', function(e) { + var name = this.parentNode.getElementsByClassName('selected-name')[0].innerHTML; + self._upSelectedOverlay(name); + }); + L.DomEvent.addListener(selectedDown, 'click', function(e) { + var name = this.parentNode.getElementsByClassName('selected-name')[0].innerHTML; + self._downSelectedOverlay(name); + }); + + } + + }, + + _removeSelectedOverlay: function(name) { + if (this.selectedOverlays.indexOf(name) > -1) { + this.selectedOverlays.splice(this.selectedOverlays.indexOf(name), 1); + this._buildSelectedOverlays(); + } + }, + + _upSelectedOverlay: function(name) { + var overlays = this.selectedOverlays; + var selectedIndex = overlays.indexOf(name); + var upSelectedIndex = selectedIndex - 1; + if (upSelectedIndex > -1) { + var tempLayer = overlays[selectedIndex]; + overlays[selectedIndex] = overlays[upSelectedIndex]; + overlays[upSelectedIndex] = tempLayer; + } + this.selectedOverlays = overlays; + return this._reOrderOverlay(); + }, + + _downSelectedOverlay: function(name) { + var overlays = this.selectedOverlays; + var selectedIndex = overlays.indexOf(name); + var upSelectedIndex = selectedIndex; + upSelectedIndex++; + if (upSelectedIndex < overlays.length) { + var tempLayer = overlays[upSelectedIndex]; + overlays[upSelectedIndex] = overlays[selectedIndex]; + overlays[selectedIndex] = tempLayer; + } + this.selectedOverlays = overlays; + return this._reOrderOverlay(); + }, + + _reOrderOverlay: function() { + var self = this; + var zIndexBase = this.zIndexBase; + var overlays = this.selectedOverlays; + var totalSelected = overlays.length; + var maxBase = zIndexBase + totalSelected; + for (var i = 0; i < overlays.length; i++) { + var layerName = overlays[i]; + var layer = self._getLayerByName(layerName).layer; + layer.setZIndex(maxBase); + maxBase--; + }; + return this._buildSelectedOverlays(); + }, + + _selectOverlays: function() { + var selectedOverlays = this.mapConfig.selectedOverlays; + var overLays = this.overLays; + for (var i = 0; i < selectedOverlays.length; i++) { + var overlay = selectedOverlays[i]; + if (overLays[overlay]) { + overLays[overlay].addTo(map); + this._addSelectedOverlay(selectedOverlays[i]); + } + } + }, + _addLayer: function(layer, name, overlay) { + var id = L.stamp(layer); + + this._layers[id] = { + layer: layer, + name: name, + overlay: overlay + }; + + if (this.options.autoZIndex && layer.setZIndex && overlay) { + this._lastZIndex++; + layer.setZIndex(this._lastZIndex); + } else { + layer.setZIndex(0); + } + }, + + _update: function() { + if (!this._container) { + return; + } + + this._baseLayersList.innerHTML = ''; + this._overlaysList.innerHTML = ''; + + var baseLayersPresent = false, + overlaysPresent = false, + i, obj; + + for (i in this._layers) { + obj = this._layers[i]; + this._addItem(obj); + overlaysPresent = overlaysPresent || obj.overlay; + baseLayersPresent = baseLayersPresent || !obj.overlay; + } + + this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none'; + }, + + _onLayerChange: function(e) { + var obj = this._layers[L.stamp(e.layer)]; + var self = this; + if (!obj) { + return; + } + + if (!this._handlingClick) { + this._update(); + } + + var type = obj.overlay ? + (e.type === 'layeradd' ? 'overlayadd' : 'overlayremove') : + (e.type === 'layeradd' ? 'baselayerchange' : null); + + if (type) { + this._map.fire(type, obj); + } + + if (type === 'overlayadd') { + this._addSelectedOverlay(obj.name); + e.layer.setZIndex(this.zIndexBase) + } + + if (type === 'overlayremove') { + this._removeSelectedOverlay(obj.name); + } + }, + + // IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe) + _createRadioElement: function(name, checked) { + + var radioHtml = ' ')); + } + + this._container.innerHTML = prefixAndAttribs.join(' | '); + } +}); + +// Simple AJAX helper (since we can't assume jQuery etc. are present) +//credit to Houston Engineering, INC www.heigeo.com +function ajax(url, callback) { + var context = this, + request = new XMLHttpRequest(); + request.onreadystatechange = change; + request.open('GET', url, true); + request.send(); + + function change() { + if (request.readyState === 4) { + if (request.status === 200) { + callback.call(context, request.responseText); + } else { + callback.call(context, "error"); + } + } + } +}; + +// Below is a version of x2js +/* + Copyright 2011-2013 Abdulla Abdurakhmanov + Original sources are available at https://code.google.com/p/x2js/ + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +(function(a, b) { + if (typeof define === "function" && define.amd) { + define([], b); + } else { + if (typeof exports === "object") { + module.exports = b(); + } else { + a.X2JS = b(); + } + } +}(this, function() { + return function(z) { + var t = "1.2.0"; + z = z || {}; + i(); + u(); + + function i() { + if (z.escapeMode === undefined) { + z.escapeMode = true; + } + z.attributePrefix = z.attributePrefix || "_"; + z.arrayAccessForm = z.arrayAccessForm || "none"; + z.emptyNodeForm = z.emptyNodeForm || "text"; + if (z.enableToStringFunc === undefined) { + z.enableToStringFunc = true; + } + z.arrayAccessFormPaths = z.arrayAccessFormPaths || []; + if (z.skipEmptyTextNodesForObj === undefined) { + z.skipEmptyTextNodesForObj = true; + } + if (z.stripWhitespaces === undefined) { + z.stripWhitespaces = true; + } + z.datetimeAccessFormPaths = z.datetimeAccessFormPaths || []; + if (z.useDoubleQuotes === undefined) { + z.useDoubleQuotes = false; + } + z.xmlElementsFilter = z.xmlElementsFilter || []; + z.jsonPropertiesFilter = z.jsonPropertiesFilter || []; + if (z.keepCData === undefined) { + z.keepCData = false; + } + } + var h = { + ELEMENT_NODE: 1, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9 + }; + + function u() {} + + function x(B) { + var C = B.localName; + if (C == null) { + C = B.baseName; + } + if (C == null || C == "") { + C = B.nodeName; + } + return C; + } + + function r(B) { + return B.prefix; + } + + function s(B) { + if (typeof(B) == "string") { + return B.replace(/&/g, "&").replace(//g, ">").replace(/"/g, + """).replace(/'/g, "'"); + } else { + return B; + } + } + + function k(B) { + return B.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace( + /'/g, "'").replace(/&/g, "&"); + } + + function w(C, F, D, E) { + var B = 0; + for (; B < C.length; B++) { + var G = C[B]; + if (typeof G === "string") { + if (G == E) { + break; + } + } else { + if (G instanceof RegExp) { + if (G.test(E)) { + break; + } + } else { + if (typeof G === "function") { + if (G(F, D, E)) { + break; + } + } + } + } + } + return B != C.length; + } + + function n(D, B, C) { + switch (z.arrayAccessForm) { + case "property": + if (!(D[B] instanceof Array)) { + D[B + "_asArray"] = [D[B]]; + } else { + D[B + "_asArray"] = D[B]; + } + break; + } + if (!(D[B] instanceof Array) && z.arrayAccessFormPaths.length > 0) { + if (w(z.arrayAccessFormPaths, D, B, C)) { + D[B] = [D[B]]; + } + } + } + + function a(G) { + var E = G.split(/[-T:+Z]/g); + var F = new Date(E[0], E[1] - 1, E[2]); + var D = E[5].split("."); + F.setHours(E[3], E[4], D[0]); + if (D.length > 1) { + F.setMilliseconds(D[1]); + } + if (E[6] && E[7]) { + var C = E[6] * 60 + Number(E[7]); + var B = /\d\d-\d\d:\d\d$/.test(G) ? "-" : "+"; + C = 0 + (B == "-" ? -1 * C : C); + F.setMinutes(F.getMinutes() - C - F.getTimezoneOffset()); + } else { + if (G.indexOf("Z", G.length - 1) !== -1) { + F = new Date(Date.UTC(F.getFullYear(), F.getMonth(), F.getDate(), F.getHours(), F.getMinutes(), + F.getSeconds(), F.getMilliseconds())); + } + } + return F; + } + + function q(D, B, C) { + if (z.datetimeAccessFormPaths.length > 0) { + var E = C.split(".#")[0]; + if (w(z.datetimeAccessFormPaths, D, B, E)) { + return a(D); + } else { + return D; + } + } else { + return D; + } + } + + function b(E, C, B, D) { + if (C == h.ELEMENT_NODE && z.xmlElementsFilter.length > 0) { + return w(z.xmlElementsFilter, E, B, D); + } else { + return true; + } + } + + function A(D, J) { + if (D.nodeType == h.DOCUMENT_NODE) { + var K = new Object; + var B = D.childNodes; + for (var L = 0; L < B.length; L++) { + var C = B.item(L); + if (C.nodeType == h.ELEMENT_NODE) { + var I = x(C); + K[I] = A(C, I); + } + } + return K; + } else { + if (D.nodeType == h.ELEMENT_NODE) { + var K = new Object; + K.__cnt = 0; + var B = D.childNodes; + for (var L = 0; L < B.length; L++) { + var C = B.item(L); + var I = x(C); + if (C.nodeType != h.COMMENT_NODE) { + var H = J + "." + I; + if (b(K, C.nodeType, I, H)) { + K.__cnt++; + if (K[I] == null) { + K[I] = A(C, H); + n(K, I, H); + } else { + if (K[I] != null) { + if (!(K[I] instanceof Array)) { + K[I] = [K[I]]; + n(K, I, H); + } + }(K[I])[K[I].length] = A(C, H); + } + } + } + } + for (var E = 0; E < D.attributes.length; E++) { + var F = D.attributes.item(E); + K.__cnt++; + K[z.attributePrefix + F.name] = F.value; + } + var G = r(D); + if (G != null && G != "") { + K.__cnt++; + K.__prefix = G; + } + if (K["#text"] != null) { + K.__text = K["#text"]; + if (K.__text instanceof Array) { + K.__text = K.__text.join("\n"); + } + if (z.stripWhitespaces) { + K.__text = K.__text.trim(); + } + delete K["#text"]; + if (z.arrayAccessForm == "property") { + delete K["#text_asArray"]; + } + K.__text = q(K.__text, I, J + "." + I); + } + if (K["#cdata-section"] != null) { + K.__cdata = K["#cdata-section"]; + delete K["#cdata-section"]; + if (z.arrayAccessForm == "property") { + delete K["#cdata-section_asArray"]; + } + } + if (K.__cnt == 0 && z.emptyNodeForm == "text") { + K = ""; + } else { + if (K.__cnt == 1 && K.__text != null) { + K = K.__text; + } else { + if (K.__cnt == 1 && K.__cdata != null && !z.keepCData) { + K = K.__cdata; + } else { + if (K.__cnt > 1 && K.__text != null && z.skipEmptyTextNodesForObj) { + if ((z.stripWhitespaces && K.__text == "") || (K.__text.trim() == "")) { + delete K.__text; + } + } + } + } + } + delete K.__cnt; + if (z.enableToStringFunc && (K.__text != null || K.__cdata != null)) { + K.toString = function() { + return (this.__text != null ? this.__text : "") + (this.__cdata != null ? this.__cdata : + ""); + }; + } + return K; + } else { + if (D.nodeType == h.TEXT_NODE || D.nodeType == h.CDATA_SECTION_NODE) { + return D.nodeValue; + } + } + } + } + + function o(I, F, H, C) { + var E = "<" + ((I != null && I.__prefix != null) ? (I.__prefix + ":") : "") + F; + if (H != null) { + for (var G = 0; G < H.length; G++) { + var D = H[G]; + var B = I[D]; + if (z.escapeMode) { + B = s(B); + } + E += " " + D.substr(z.attributePrefix.length) + "="; + if (z.useDoubleQuotes) { + E += '"' + B + '"'; + } else { + E += "'" + B + "'"; + } + } + } + if (!C) { + E += ">"; + } else { + E += "/>"; + } + return E; + } + + function j(C, B) { + return ""; + } + + function v(C, B) { + return C.indexOf(B, C.length - B.length) !== -1; + } + + function y(C, B) { + if ((z.arrayAccessForm == "property" && v(B.toString(), ("_asArray"))) || B.toString().indexOf( + z.attributePrefix) == 0 || B.toString().indexOf("__") == 0 || (C[B] instanceof Function)) { + return true; + } else { + return false; + } + } + + function m(D) { + var C = 0; + if (D instanceof Object) { + for (var B in D) { + if (y(D, B)) { + continue; + } + C++; + } + } + return C; + } + + function l(D, B, C) { + return z.jsonPropertiesFilter.length == 0 || C == "" || w(z.jsonPropertiesFilter, D, B, C); + } + + function c(D) { + var C = []; + if (D instanceof Object) { + for (var B in D) { + if (B.toString().indexOf("__") == -1 && B.toString().indexOf(z.attributePrefix) == 0) { + C.push(B); + } + } + } + return C; + } + + function g(C) { + var B = ""; + if (C.__cdata != null) { + B += ""; + } + if (C.__text != null) { + if (z.escapeMode) { + B += s(C.__text); + } else { + B += C.__text; + } + } + return B; + } + + function d(C) { + var B = ""; + if (C instanceof Object) { + B += g(C); + } else { + if (C != null) { + if (z.escapeMode) { + B += s(C); + } else { + B += C; + } + } + } + return B; + } + + function p(C, B) { + if (C === "") { + return B; + } else { + return C + "." + B; + } + } + + function f(D, G, F, E) { + var B = ""; + if (D.length == 0) { + B += o(D, G, F, true); + } else { + for (var C = 0; C < D.length; C++) { + B += o(D[C], G, c(D[C]), false); + B += e(D[C], p(E, G)); + B += j(D[C], G); + } + } + return B; + } + + function e(I, H) { + var B = ""; + var F = m(I); + if (F > 0) { + for (var E in I) { + if (y(I, E) || (H != "" && !l(I, E, p(H, E)))) { + continue; + } + var D = I[E]; + var G = c(D); + if (D == null || D == undefined) { + B += o(D, E, G, true); + } else { + if (D instanceof Object) { + if (D instanceof Array) { + B += f(D, E, G, H); + } else { + if (D instanceof Date) { + B += o(D, E, G, false); + B += D.toISOString(); + B += j(D, E); + } else { + var C = m(D); + if (C > 0 || D.__text != null || D.__cdata != null) { + B += o(D, E, G, false); + B += e(D, p(H, E)); + B += j(D, E); + } else { + B += o(D, E, G, true); + } + } + } + } else { + B += o(D, E, G, false); + B += d(D); + B += j(D, E); + } + } + } + } + B += d(I); + return B; + } + this.parseXmlString = function(D) { + var F = window.ActiveXObject || "ActiveXObject" in window; + if (D === undefined) { + return null; + } + var E; + if (window.DOMParser) { + var G = new window.DOMParser(); + var B = null; + if (!F) { + try { + B = G.parseFromString("INVALID", "text/xml").getElementsByTagName("parsererror")[0].namespaceURI; + } catch (C) { + B = null; + } + } + try { + E = G.parseFromString(D, "text/xml"); + if (B != null && E.getElementsByTagNameNS(B, "parsererror").length > 0) { + E = null; + } + } catch (C) { + E = null; + } + } else { + if (D.indexOf("") + 2); + } + E = new ActiveXObject("Microsoft.XMLDOM"); + E.async = "false"; + E.loadXML(D); + } + return E; + }; + this.asArray = function(B) { + if (B === undefined || B == null) { + return []; + } else { + if (B instanceof Array) { + return B; + } else { + return [B]; + } + } + }; + this.toXmlDateTime = function(B) { + if (B instanceof Date) { + return B.toISOString(); + } else { + if (typeof(B) === "number") { + return new Date(B).toISOString(); + } else { + return null; + } + } + }; + this.asDateTime = function(B) { + if (typeof(B) == "string") { + return a(B); + } else { + return B; + } + }; + this.xml2json = function(B) { + return A(B); + }; + this.xml_str2json = function(B) { + var C = this.parseXmlString(B); + if (C != null) { + return this.xml2json(C); + } else { + return null; + } + }; + this.json2xml_str = function(B) { + return e(B, ""); + }; + this.json2xml = function(C) { + var B = this.json2xml_str(C); + return this.parseXmlString(B); + }; + this.getVersion = function() { + return t; + }; + }; +})); \ No newline at end of file diff --git a/app/templates/app/base.html b/app/templates/app/base.html index 03b42827..768316b2 100644 --- a/app/templates/app/base.html +++ b/app/templates/app/base.html @@ -25,6 +25,8 @@ + {% load render_bundle from webpack_loader %} + {% render_bundle 'main' %} {{title|default:"Login"}} - WebODM @@ -109,4 +111,5 @@ $(function(){ }); + \ No newline at end of file diff --git a/app/templates/app/logged_in_base.html b/app/templates/app/logged_in_base.html index a4367f4f..58cd221d 100644 --- a/app/templates/app/logged_in_base.html +++ b/app/templates/app/logged_in_base.html @@ -1,5 +1,6 @@ {% extends "app/base.html" %} {% load i18n static %} +