Image thumbnails, downloads, camera shots display in map
|
@ -0,0 +1,67 @@
|
|||
import os
|
||||
|
||||
import io
|
||||
|
||||
from .tasks import TaskNestedView
|
||||
from rest_framework import exceptions
|
||||
from app.models import ImageUpload
|
||||
from app.models.task import assets_directory_path
|
||||
from PIL import Image
|
||||
from django.http import HttpResponse
|
||||
from .tasks import download_file_response
|
||||
|
||||
class Thumbnail(TaskNestedView):
|
||||
def get(self, request, pk=None, project_pk=None, image_filename=""):
|
||||
"""
|
||||
Generate a thumbnail on the fly for a particular task's image
|
||||
"""
|
||||
task = self.get_and_check_task(request, pk)
|
||||
image = ImageUpload.objects.filter(task=task, image=assets_directory_path(task.id, task.project.id, image_filename)).first()
|
||||
|
||||
if image is None:
|
||||
raise exceptions.NotFound()
|
||||
|
||||
image_path = image.path()
|
||||
if not os.path.isfile(image_path):
|
||||
raise exceptions.NotFound()
|
||||
|
||||
try:
|
||||
thumb_size = int(self.request.query_params.get('size', 512))
|
||||
if thumb_size < 1:
|
||||
raise ValueError()
|
||||
|
||||
quality = int(self.request.query_params.get('quality', 75))
|
||||
if quality < 0 or quality > 100:
|
||||
raise ValueError()
|
||||
|
||||
except ValueError:
|
||||
raise exceptions.ValidationError("Invalid query parameters")
|
||||
|
||||
with Image.open(image_path) as img:
|
||||
img.thumbnail((thumb_size, thumb_size))
|
||||
output = io.BytesIO()
|
||||
img.save(output, format='JPEG', quality=quality)
|
||||
|
||||
res = HttpResponse(content_type="image/jpeg")
|
||||
res['Content-Disposition'] = 'inline'
|
||||
res.write(output.getvalue())
|
||||
output.close()
|
||||
|
||||
return res
|
||||
|
||||
class ImageDownload(TaskNestedView):
|
||||
def get(self, request, pk=None, project_pk=None, image_filename=""):
|
||||
"""
|
||||
Download a task's image
|
||||
"""
|
||||
task = self.get_and_check_task(request, pk)
|
||||
image = ImageUpload.objects.filter(task=task, image=assets_directory_path(task.id, task.project.id, image_filename)).first()
|
||||
|
||||
if image is None:
|
||||
raise exceptions.NotFound()
|
||||
|
||||
image_path = image.path()
|
||||
if not os.path.isfile(image_path):
|
||||
raise exceptions.NotFound()
|
||||
|
||||
return download_file_response(request, image_path, 'attachment')
|
|
@ -1,36 +0,0 @@
|
|||
import os
|
||||
|
||||
from .tasks import TaskNestedView
|
||||
from app.security import path_traversal_check
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from rest_framework import exceptions
|
||||
|
||||
class Thumbnail(TaskNestedView):
|
||||
def get(self, request, pk=None, project_pk=None, image_filename=""):
|
||||
"""
|
||||
Generate a thumbnail on the fly for a particular task's image
|
||||
"""
|
||||
task = self.get_and_check_task(request, pk)
|
||||
|
||||
image_path = task.task_path(image_filename)
|
||||
|
||||
try:
|
||||
path_traversal_check(image_path, task.task_path(""))
|
||||
except SuspiciousFileOperation:
|
||||
raise exceptions.NotFound()
|
||||
|
||||
if not os.path.isfile(image_path):
|
||||
raise exceptions.NotFound()
|
||||
|
||||
# TODO
|
||||
|
||||
return Response({
|
||||
'tilejson': '2.1.0',
|
||||
'name': task.name,
|
||||
'version': '1.0.0',
|
||||
'scheme': 'xyz',
|
||||
'tiles': [get_tile_url(task, tile_type, self.request.query_params)],
|
||||
'minzoom': minzoom - ZOOM_EXTRA_LEVELS,
|
||||
'maxzoom': maxzoom + ZOOM_EXTRA_LEVELS,
|
||||
'bounds': get_extent(task, tile_type).extent
|
||||
})
|
|
@ -4,6 +4,7 @@ from app.api.presets import PresetViewSet
|
|||
from app.plugins.views import api_view_handler
|
||||
from .projects import ProjectViewSet
|
||||
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskAssetsImport
|
||||
from .imageuploads import Thumbnail, ImageDownload
|
||||
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
|
||||
from .admin import UserViewSet, GroupViewSet
|
||||
from rest_framework_nested import routers
|
||||
|
@ -31,8 +32,6 @@ urlpatterns = [
|
|||
url(r'^', include(tasks_router.urls)),
|
||||
url(r'^', include(admin_router.urls)),
|
||||
|
||||
|
||||
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/tiles\.json$', TileJson.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/bounds$', Bounds.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/(?P<tile_type>orthophoto|dsm|dtm)/metadata$', Metadata.as_view()),
|
||||
|
@ -43,6 +42,8 @@ urlpatterns = [
|
|||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/import$', TaskAssetsImport.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/images/thumbnail/(?P<image_filename>.+)$', Thumbnail.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/images/download/(?P<image_filename>.+)$', ImageDownload.as_view()),
|
||||
|
||||
url(r'workers/check/(?P<celery_task_id>.+)', CheckTask.as_view()),
|
||||
url(r'workers/get/(?P<celery_task_id>.+)', GetTaskResult.as_view()),
|
||||
|
|
|
@ -760,13 +760,17 @@ class Task(models.Model):
|
|||
if 'dsm.tif' in self.available_assets: types.append('dsm')
|
||||
if 'dtm.tif' in self.available_assets: types.append('dtm')
|
||||
|
||||
camera_shots = ''
|
||||
if 'shots.geojson' in self.available_assets: camera_shots = '/api/projects/{}/tasks/{}/download/shots.geojson'.format(self.project.id, self.id)
|
||||
|
||||
return {
|
||||
'tiles': [{'url': self.get_tile_base_url(t), 'type': t} for t in types],
|
||||
'meta': {
|
||||
'task': {
|
||||
'id': str(self.id),
|
||||
'project': self.project.id,
|
||||
'public': self.public
|
||||
'public': self.public,
|
||||
'camera_shots': camera_shots
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AssetDownloads from '../classes/AssetDownloads';
|
||||
|
||||
class ImagePopup extends React.Component {
|
||||
static propTypes = {
|
||||
feature: PropTypes.object.isRequired,
|
||||
task: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: "",
|
||||
}
|
||||
}
|
||||
|
||||
imageOnError = () => {
|
||||
this.setState({error: "Image is missing"});
|
||||
}
|
||||
|
||||
render(){
|
||||
const { error } = this.state;
|
||||
const { feature, task } = this.props;
|
||||
|
||||
const downloadImageLink = `/api/projects/${task.project}/tasks/${task.id}/images/download/${feature.properties.filename}`;
|
||||
const thumbUrl = `/api/projects/${task.project}/tasks/${task.id}/images/thumbnail/${feature.properties.filename}?size=320`;
|
||||
const downloadShotsLink = `/api/projects/${task.project}/tasks/${task.id}/download/shots.geojson`;
|
||||
|
||||
const assetDownload = AssetDownloads.only(["shots.geojson"])[0];
|
||||
|
||||
return (<div>
|
||||
<strong>{feature.properties.filename}</strong>
|
||||
{error !== "" ? <div style={{marginTop: "8px"}}>{error}</div>
|
||||
: [
|
||||
<div key="image" style={{marginTop: "8px"}}>
|
||||
<a href={downloadImageLink} title={feature.properties.filename}><img style={{borderRadius: "4px"}} src={thumbUrl} onError={this.imageOnError} /></a>
|
||||
</div>,
|
||||
<div key="download-image" style={{marginTop: "8px"}}>
|
||||
<a href={downloadImageLink}><i className="fa fa-image"></i> Download Image</a>
|
||||
</div>
|
||||
]}
|
||||
<div style={{marginTop: "8px"}}>
|
||||
<a href={downloadShotsLink}><i className={assetDownload.icon}></i> {assetDownload.label} </a>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
export default ImagePopup;
|
|
@ -13,15 +13,13 @@ export default class LayersControlLayer extends React.Component {
|
|||
layer: null,
|
||||
expanded: false,
|
||||
map: null,
|
||||
overlay: false,
|
||||
overlayIcon: "fa fa-vector-square fa-fw"
|
||||
overlay: false
|
||||
};
|
||||
static propTypes = {
|
||||
layer: PropTypes.object.isRequired,
|
||||
expanded: PropTypes.bool,
|
||||
map: PropTypes.object.isRequired,
|
||||
overlay: PropTypes.bool,
|
||||
overlayIcon: PropTypes.string
|
||||
overlay: PropTypes.bool
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
|
@ -264,8 +262,8 @@ export default class LayersControlLayer extends React.Component {
|
|||
}
|
||||
|
||||
return (<div className="layers-control-layer">
|
||||
{!this.props.overlay ? <ExpandButton bind={[this, 'expanded']} /> : <div className="overlayIcon"><i className={this.props.overlayIcon}></i></div>}<Checkbox bind={[this, 'visible']}/>
|
||||
<a className="layer-label" href="javascript:void(0);" onClick={this.handleLayerClick}>{meta.name}</a>
|
||||
{!this.props.overlay ? <ExpandButton bind={[this, 'expanded']} /> : <div className="overlayIcon"><i className={meta.icon || "fa fa-vector-square fa-fw"}></i></div>}<Checkbox bind={[this, 'visible']}/>
|
||||
<a title={meta.name} className="layer-label" href="javascript:void(0);" onClick={this.handleLayerClick}>{meta.name}</a>
|
||||
|
||||
{this.state.expanded ?
|
||||
<div className="layer-expanded">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'ReactDOM';
|
||||
import '../css/Map.scss';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import Leaflet from 'leaflet';
|
||||
|
@ -11,6 +12,7 @@ import '../vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers';
|
|||
import Dropzone from '../vendor/dropzone';
|
||||
import $ from 'jquery';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import ImagePopup from './ImagePopup';
|
||||
import SwitchModeButton from './SwitchModeButton';
|
||||
import ShareButton from './ShareButton';
|
||||
import AssetDownloads from '../classes/AssetDownloads';
|
||||
|
@ -22,6 +24,8 @@ import Standby from './Standby';
|
|||
import LayersControl from './LayersControl';
|
||||
import update from 'immutability-helper';
|
||||
import Utils from '../classes/Utils';
|
||||
import '../vendor/leaflet/Leaflet.Ajax';
|
||||
import '../vendor/leaflet/Leaflet.Awesome-markers';
|
||||
|
||||
class Map extends React.Component {
|
||||
static defaultProps = {
|
||||
|
@ -53,6 +57,7 @@ class Map extends React.Component {
|
|||
this.basemaps = {};
|
||||
this.mapBounds = null;
|
||||
this.autolayers = null;
|
||||
this.addedCameraShots = false;
|
||||
|
||||
this.loadImageryLayers = this.loadImageryLayers.bind(this);
|
||||
this.updatePopupFor = this.updatePopupFor.bind(this);
|
||||
|
@ -70,6 +75,20 @@ class Map extends React.Component {
|
|||
$('#layerOpacity', popup.getContent()).val(layer.options.opacity);
|
||||
}
|
||||
|
||||
typeToHuman = (type) => {
|
||||
switch(type){
|
||||
case "orthophoto":
|
||||
return "Orthophoto";
|
||||
case "plant":
|
||||
return "Plant Health";
|
||||
case "dsm":
|
||||
return "DSM";
|
||||
case "dtm":
|
||||
return "DTM";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
loadImageryLayers(forceAddLayers = false){
|
||||
// Cancel previous requests
|
||||
if (this.tileJsonRequests) {
|
||||
|
@ -141,7 +160,7 @@ class Map extends React.Component {
|
|||
});
|
||||
|
||||
// Associate metadata with this layer
|
||||
meta.name = name;
|
||||
meta.name = name + ` (${this.typeToHuman(type)})`;
|
||||
meta.metaUrl = metaUrl;
|
||||
layer[Symbol.for("meta")] = meta;
|
||||
layer[Symbol.for("tile-meta")] = mres;
|
||||
|
@ -197,6 +216,54 @@ class Map extends React.Component {
|
|||
mapBounds.extend(bounds);
|
||||
this.mapBounds = mapBounds;
|
||||
|
||||
// Add camera shots layer if available
|
||||
if (meta.task && meta.task.camera_shots && !this.addedCameraShots){
|
||||
const cameraMarker = L.AwesomeMarkers.icon({
|
||||
icon: 'camera',
|
||||
markerColor: 'blue',
|
||||
prefix: 'fa'
|
||||
});
|
||||
|
||||
const shotsLayer = new L.GeoJSON.AJAX(meta.task.camera_shots, {
|
||||
style: function (feature) {
|
||||
return {
|
||||
opacity: 1,
|
||||
fillOpacity: 0.7,
|
||||
color: "#000000"
|
||||
}
|
||||
},
|
||||
pointToLayer: function (feature, latlng) {
|
||||
return L.marker(latlng, {
|
||||
icon: cameraMarker
|
||||
});
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
if (feature.properties && feature.properties.filename) {
|
||||
let root = null;
|
||||
const lazyrender = () => {
|
||||
if (!root) root = document.createElement("div");
|
||||
ReactDOM.render(<ImagePopup task={meta.task} feature={feature}/>, root);
|
||||
return root;
|
||||
}
|
||||
|
||||
layer.bindPopup(L.popup(
|
||||
{
|
||||
lazyrender,
|
||||
maxHeight: 450,
|
||||
minWidth: 320
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
shotsLayer[Symbol.for("meta")] = {name: name + " (Cameras)", icon: "fa fa-camera fa-fw"};
|
||||
|
||||
this.setState(update(this.state, {
|
||||
overlays: {$push: [shotsLayer]}
|
||||
}));
|
||||
|
||||
this.addedCameraShots = true;
|
||||
}
|
||||
|
||||
done();
|
||||
})
|
||||
.fail((_, __, err) => done(err))
|
||||
|
@ -359,7 +426,7 @@ https://a.tile.openstreetmap.org/{z}/{x}/{y}.png
|
|||
}
|
||||
}).on('popupopen', e => {
|
||||
// Load task assets links in popup
|
||||
if (e.popup && e.popup._source && e.popup._content){
|
||||
if (e.popup && e.popup._source && e.popup._content && !e.popup.options.lazyrender){
|
||||
const infoWindow = e.popup._content;
|
||||
if (typeof infoWindow === 'string') return;
|
||||
|
||||
|
@ -387,6 +454,10 @@ https://a.tile.openstreetmap.org/{z}/{x}/{y}.png
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (e.popup && e.popup.options.lazyrender){
|
||||
e.popup.setContent(e.popup.options.lazyrender());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import ImagePopup from '../ImagePopup';
|
||||
|
||||
describe('<ImagePopup />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const wrapper = mount(<ImagePopup task={{id: 1, project: 1}} feature={{properties: {filename: "abc"}}} />);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
})
|
||||
});
|
|
@ -91,4 +91,10 @@
|
|||
.leaflet-touch .leaflet-control-add-overlay a, .leaflet-control-add-overlay a {
|
||||
background-position: 2px 2px;
|
||||
}
|
||||
.leaflet-container{
|
||||
a.leaflet-popup-close-button{
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
'use strict';
|
||||
var jsonp = require('./jsonp');
|
||||
var Promise = require('lie');
|
||||
|
||||
module.exports = function (url, options) {
|
||||
options = options || {};
|
||||
if (options.jsonp) {
|
||||
return jsonp(url, options);
|
||||
}
|
||||
var request;
|
||||
var cancel;
|
||||
var out = new Promise(function (resolve, reject) {
|
||||
cancel = reject;
|
||||
if (global.XMLHttpRequest === undefined) {
|
||||
reject('XMLHttpRequest is not supported');
|
||||
}
|
||||
var response;
|
||||
request = new global.XMLHttpRequest();
|
||||
request.open('GET', url);
|
||||
if (options.headers) {
|
||||
Object.keys(options.headers).forEach(function (key) {
|
||||
request.setRequestHeader(key, options.headers[key]);
|
||||
});
|
||||
}
|
||||
request.onreadystatechange = function () {
|
||||
if (request.readyState === 4) {
|
||||
if ((request.status < 400 && options.local) || request.status === 200) {
|
||||
if (global.JSON) {
|
||||
response = JSON.parse(request.responseText);
|
||||
} else {
|
||||
reject(new Error('JSON is not supported'));
|
||||
}
|
||||
resolve(response);
|
||||
} else {
|
||||
if (!request.status) {
|
||||
reject('Attempted cross origin request without CORS enabled');
|
||||
} else {
|
||||
reject(request.statusText);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
request.send();
|
||||
});
|
||||
out.catch(function (reason) {
|
||||
request.abort();
|
||||
return reason;
|
||||
});
|
||||
out.abort = cancel;
|
||||
return out;
|
||||
};
|
|
@ -0,0 +1,133 @@
|
|||
'use strict';
|
||||
var L = global.L || require('leaflet');
|
||||
var Promise = require('lie');
|
||||
var ajax = require('./ajax');
|
||||
L.GeoJSON.AJAX = L.GeoJSON.extend({
|
||||
defaultAJAXparams: {
|
||||
dataType: 'json',
|
||||
callbackParam: 'callback',
|
||||
local: false,
|
||||
middleware: function (f) {
|
||||
return f;
|
||||
}
|
||||
},
|
||||
initialize: function (url, options) {
|
||||
this.urls = [];
|
||||
if (url) {
|
||||
if (typeof url === 'string') {
|
||||
this.urls.push(url);
|
||||
} else if (typeof url.pop === 'function') {
|
||||
this.urls = this.urls.concat(url);
|
||||
} else {
|
||||
options = url;
|
||||
url = undefined;
|
||||
}
|
||||
}
|
||||
var ajaxParams = L.Util.extend({}, this.defaultAJAXparams);
|
||||
|
||||
for (var i in options) {
|
||||
if (this.defaultAJAXparams.hasOwnProperty(i)) {
|
||||
ajaxParams[i] = options[i];
|
||||
}
|
||||
}
|
||||
this.ajaxParams = ajaxParams;
|
||||
this._layers = {};
|
||||
L.Util.setOptions(this, options);
|
||||
this.on('data:loaded', function () {
|
||||
if (this.filter) {
|
||||
this.refilter(this.filter);
|
||||
}
|
||||
}, this);
|
||||
var self = this;
|
||||
if (this.urls.length > 0) {
|
||||
new Promise(function (resolve) {
|
||||
resolve();
|
||||
}).then(function () {
|
||||
self.addUrl();
|
||||
});
|
||||
}
|
||||
},
|
||||
clearLayers: function () {
|
||||
this.urls = [];
|
||||
L.GeoJSON.prototype.clearLayers.call(this);
|
||||
return this;
|
||||
},
|
||||
addUrl: function (url) {
|
||||
var self = this;
|
||||
if (url) {
|
||||
if (typeof url === 'string') {
|
||||
self.urls.push(url);
|
||||
} else if (typeof url.pop === 'function') {
|
||||
self.urls = self.urls.concat(url);
|
||||
}
|
||||
}
|
||||
var loading = self.urls.length;
|
||||
var done = 0;
|
||||
self.fire('data:loading');
|
||||
self.urls.forEach(function (url) {
|
||||
if (self.ajaxParams.dataType.toLowerCase() === 'json') {
|
||||
ajax(url, self.ajaxParams).then(function (d) {
|
||||
var data = self.ajaxParams.middleware(d);
|
||||
self.addData(data);
|
||||
self.fire('data:progress', data);
|
||||
}, function (err) {
|
||||
self.fire('data:progress', {
|
||||
error: err
|
||||
});
|
||||
});
|
||||
} else if (self.ajaxParams.dataType.toLowerCase() === 'jsonp') {
|
||||
L.Util.jsonp(url, self.ajaxParams).then(function (d) {
|
||||
var data = self.ajaxParams.middleware(d);
|
||||
self.addData(data);
|
||||
self.fire('data:progress', data);
|
||||
}, function (err) {
|
||||
self.fire('data:progress', {
|
||||
error: err
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
self.on('data:progress', function () {
|
||||
if (++done === loading) {
|
||||
self.fire('data:loaded');
|
||||
}
|
||||
});
|
||||
},
|
||||
refresh: function (url) {
|
||||
url = url || this.urls;
|
||||
this.clearLayers();
|
||||
this.addUrl(url);
|
||||
},
|
||||
refilter: function (func) {
|
||||
if (typeof func !== 'function') {
|
||||
this.filter = false;
|
||||
this.eachLayer(function (a) {
|
||||
a.setStyle({
|
||||
stroke: true,
|
||||
clickable: true
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.filter = func;
|
||||
this.eachLayer(function (a) {
|
||||
if (func(a.feature)) {
|
||||
a.setStyle({
|
||||
stroke: true,
|
||||
clickable: true
|
||||
});
|
||||
} else {
|
||||
a.setStyle({
|
||||
stroke: false,
|
||||
clickable: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
L.Util.Promise = Promise;
|
||||
L.Util.ajax = ajax;
|
||||
L.Util.jsonp = require('./jsonp');
|
||||
L.geoJson.ajax = function (geojson, options) {
|
||||
return new L.GeoJSON.AJAX(geojson, options);
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
'use strict';
|
||||
var L = global.L || require('leaflet');
|
||||
var Promise = require('lie');
|
||||
|
||||
module.exports = function (url, options) {
|
||||
options = options || {};
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
var scriptNode = L.DomUtil.create('script', '', head);
|
||||
var cbName, ourl, cbSuffix, cancel;
|
||||
var out = new Promise(function (resolve, reject) {
|
||||
cancel = reject;
|
||||
var cbParam = options.cbParam || 'callback';
|
||||
if (options.callbackName) {
|
||||
cbName = options.callbackName;
|
||||
} else {
|
||||
cbSuffix = '_' + ('' + Math.random()).slice(2);
|
||||
cbName = '_leafletJSONPcallbacks.' + cbSuffix;
|
||||
}
|
||||
scriptNode.type = 'text/javascript';
|
||||
if (cbSuffix) {
|
||||
if (!global._leafletJSONPcallbacks) {
|
||||
global._leafletJSONPcallbacks = {
|
||||
length: 0
|
||||
};
|
||||
}
|
||||
global._leafletJSONPcallbacks.length++;
|
||||
global._leafletJSONPcallbacks[cbSuffix] = function (data) {
|
||||
head.removeChild(scriptNode);
|
||||
delete global._leafletJSONPcallbacks[cbSuffix];
|
||||
global._leafletJSONPcallbacks.length--;
|
||||
if (!global._leafletJSONPcallbacks.length) {
|
||||
delete global._leafletJSONPcallbacks;
|
||||
}
|
||||
resolve(data);
|
||||
};
|
||||
}
|
||||
if (url.indexOf('?') === -1) {
|
||||
ourl = url + '?' + cbParam + '=' + cbName;
|
||||
} else {
|
||||
ourl = url + '&' + cbParam + '=' + cbName;
|
||||
}
|
||||
scriptNode.src = ourl;
|
||||
}).then(null, function (reason) {
|
||||
head.removeChild(scriptNode);
|
||||
delete L.Util.ajax.cb[cbSuffix];
|
||||
return reason;
|
||||
});
|
||||
out.abort = cancel;
|
||||
return out;
|
||||
};
|
Po Szerokość: | Wysokość: | Rozmiar: 14 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 30 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 7.8 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 535 B |
Po Szerokość: | Wysokość: | Rozmiar: 1.4 KiB |
BIN
app/static/app/js/vendor/leaflet/Leaflet.Awesome-markers/images/markers-soft.png
vendored
100644
Po Szerokość: | Wysokość: | Rozmiar: 40 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 65 KiB |
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
Leaflet.AwesomeMarkers, a plugin that adds colorful iconic markers for Leaflet, based on the Font Awesome icons
|
||||
(c) 2012-2013, Lennard Voogdt
|
||||
|
||||
http://leafletjs.com
|
||||
https://github.com/lvoogdt
|
||||
*/
|
||||
|
||||
/*global L*/
|
||||
|
||||
import "./leaflet.awesome-markers.css";
|
||||
|
||||
(function (window, document, undefined) {
|
||||
"use strict";
|
||||
/*
|
||||
* Leaflet.AwesomeMarkers assumes that you have already included the Leaflet library.
|
||||
*/
|
||||
|
||||
L.AwesomeMarkers = {};
|
||||
|
||||
L.AwesomeMarkers.version = '2.0.1';
|
||||
|
||||
L.AwesomeMarkers.Icon = L.Icon.extend({
|
||||
options: {
|
||||
iconSize: [35, 45],
|
||||
iconAnchor: [17, 42],
|
||||
popupAnchor: [1, -32],
|
||||
shadowAnchor: [10, 12],
|
||||
shadowSize: [36, 16],
|
||||
className: 'awesome-marker',
|
||||
prefix: 'glyphicon',
|
||||
spinClass: 'fa-spin',
|
||||
extraClasses: '',
|
||||
icon: 'home',
|
||||
markerColor: 'blue',
|
||||
iconColor: 'white'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
options = L.Util.setOptions(this, options);
|
||||
},
|
||||
|
||||
createIcon: function () {
|
||||
var div = document.createElement('div'),
|
||||
options = this.options;
|
||||
|
||||
if (options.icon) {
|
||||
div.innerHTML = this._createInner();
|
||||
}
|
||||
|
||||
if (options.bgPos) {
|
||||
div.style.backgroundPosition =
|
||||
(-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px';
|
||||
}
|
||||
|
||||
this._setIconStyles(div, 'icon-' + options.markerColor);
|
||||
return div;
|
||||
},
|
||||
|
||||
_createInner: function() {
|
||||
var iconClass, iconSpinClass = "", iconColorClass = "", iconColorStyle = "", options = this.options;
|
||||
|
||||
if(options.icon.slice(0,options.prefix.length+1) === options.prefix + "-") {
|
||||
iconClass = options.icon;
|
||||
} else {
|
||||
iconClass = options.prefix + "-" + options.icon;
|
||||
}
|
||||
|
||||
if(options.spin && typeof options.spinClass === "string") {
|
||||
iconSpinClass = options.spinClass;
|
||||
}
|
||||
|
||||
if(options.iconColor) {
|
||||
if(options.iconColor === 'white' || options.iconColor === 'black') {
|
||||
iconColorClass = "icon-" + options.iconColor;
|
||||
} else {
|
||||
iconColorStyle = "style='color: " + options.iconColor + "' ";
|
||||
}
|
||||
}
|
||||
|
||||
return "<i " + iconColorStyle + "class='" + options.extraClasses + " " + options.prefix + " " + iconClass + " " + iconSpinClass + " " + iconColorClass + "'></i>";
|
||||
},
|
||||
|
||||
_setIconStyles: function (img, name) {
|
||||
var options = this.options,
|
||||
size = L.point(options[name === 'shadow' ? 'shadowSize' : 'iconSize']),
|
||||
anchor;
|
||||
|
||||
if (name === 'shadow') {
|
||||
anchor = L.point(options.shadowAnchor || options.iconAnchor);
|
||||
} else {
|
||||
anchor = L.point(options.iconAnchor);
|
||||
}
|
||||
|
||||
if (!anchor && size) {
|
||||
anchor = size.divideBy(2, true);
|
||||
}
|
||||
|
||||
img.className = 'awesome-marker-' + name + ' ' + options.className;
|
||||
|
||||
if (anchor) {
|
||||
img.style.marginLeft = (-anchor.x) + 'px';
|
||||
img.style.marginTop = (-anchor.y) + 'px';
|
||||
}
|
||||
|
||||
if (size) {
|
||||
img.style.width = size.x + 'px';
|
||||
img.style.height = size.y + 'px';
|
||||
}
|
||||
},
|
||||
|
||||
createShadow: function () {
|
||||
var div = document.createElement('div');
|
||||
|
||||
this._setIconStyles(div, 'shadow');
|
||||
return div;
|
||||
}
|
||||
});
|
||||
|
||||
L.AwesomeMarkers.icon = function (options) {
|
||||
return new L.AwesomeMarkers.Icon(options);
|
||||
};
|
||||
|
||||
}(this, document));
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
Author: L. Voogdt
|
||||
License: MIT
|
||||
Version: 1.0
|
||||
*/
|
||||
|
||||
/* Marker setup */
|
||||
.awesome-marker {
|
||||
background: url('images/markers-soft.png') no-repeat 0 0;
|
||||
width: 35px;
|
||||
height: 46px;
|
||||
position:absolute;
|
||||
left:0;
|
||||
top:0;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.awesome-marker-shadow {
|
||||
background: url('images/markers-shadow.png') no-repeat 0 0;
|
||||
width: 36px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Retina displays */
|
||||
@media (min--moz-device-pixel-ratio: 1.5),(-o-min-device-pixel-ratio: 3/2),
|
||||
(-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5),(min-resolution: 1.5dppx) {
|
||||
.awesome-marker {
|
||||
background-image: url('images/markers-soft@2x.png');
|
||||
background-size: 720px 46px;
|
||||
}
|
||||
.awesome-marker-shadow {
|
||||
background-image: url('images/markers-shadow@2x.png');
|
||||
background-size: 35px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.awesome-marker i {
|
||||
color: #333;
|
||||
margin-top: 10px;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.awesome-marker .icon-white {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Colors */
|
||||
.awesome-marker-icon-red {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-darkred {
|
||||
background-position: -180px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-lightred {
|
||||
background-position: -360px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-orange {
|
||||
background-position: -36px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-beige {
|
||||
background-position: -396px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-green {
|
||||
background-position: -72px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-darkgreen {
|
||||
background-position: -252px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-lightgreen {
|
||||
background-position: -432px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-blue {
|
||||
background-position: -108px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-darkblue {
|
||||
background-position: -216px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-lightblue {
|
||||
background-position: -468px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-purple {
|
||||
background-position: -144px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-darkpurple {
|
||||
background-position: -288px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-pink {
|
||||
background-position: -504px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-cadetblue {
|
||||
background-position: -324px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-white {
|
||||
background-position: -574px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-gray {
|
||||
background-position: -648px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-lightgray {
|
||||
background-position: -612px 0;
|
||||
}
|
||||
|
||||
.awesome-marker-icon-black {
|
||||
background-position: -682px 0;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "WebODM",
|
||||
"version": "1.3.6",
|
||||
"version": "1.4.0",
|
||||
"description": "User-friendly, extendable application and API for processing aerial imagery.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|