diff --git a/app/models/task.py b/app/models/task.py index 0a9b6935c..66dd5dda9 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -444,6 +444,10 @@ def duplicate(self, set_new_name=True): logger.warning("Task {} doesn't have folder, will skip copying".format(self)) self.project.owner.profile.clear_used_quota_cache() + + from app.plugins import signals as plugin_signals + plugin_signals.task_duplicated.send_robust(sender=self.__class__, task_id=task.id) + return task except Exception as e: logger.warning("Cannot duplicate task: {}".format(str(e))) @@ -936,6 +940,7 @@ def get_map_items(self): 'meta': { 'task': { 'id': str(self.id), + 'name': self.name, 'project': self.project.id, 'public': self.public, 'camera_shots': camera_shots, diff --git a/app/plugins/signals.py b/app/plugins/signals.py index cf7494c28..e613c6f83 100644 --- a/app/plugins/signals.py +++ b/app/plugins/signals.py @@ -5,5 +5,6 @@ task_removed = django.dispatch.Signal(providing_args=["task_id"]) task_failed = django.dispatch.Signal(providing_args=["task_id"]) task_resizing_images = django.dispatch.Signal(providing_args=["task_id"]) +task_duplicated = django.dispatch.Signal(providing_args=["task_id"]) processing_node_removed = django.dispatch.Signal(providing_args=["processing_node_id"]) diff --git a/app/static/app/js/MapView.jsx b/app/static/app/js/MapView.jsx index 2a69a98b9..10931964f 100644 --- a/app/static/app/js/MapView.jsx +++ b/app/static/app/js/MapView.jsx @@ -11,7 +11,8 @@ class MapView extends React.Component { selectedMapType: 'auto', title: "", public: false, - shareButtons: true + shareButtons: true, + permissions: ["view"] }; static propTypes = { @@ -19,7 +20,8 @@ class MapView extends React.Component { selectedMapType: PropTypes.oneOf(['auto', 'orthophoto', 'plant', 'dsm', 'dtm']), title: PropTypes.string, public: PropTypes.bool, - shareButtons: PropTypes.bool + shareButtons: PropTypes.bool, + permissions: PropTypes.array }; constructor(props){ @@ -130,6 +132,7 @@ class MapView extends React.Component { mapType={this.state.selectedMapType} public={this.props.public} shareButtons={this.props.shareButtons} + permissions={this.props.permissions} /> ); diff --git a/app/static/app/js/classes/Units.js b/app/static/app/js/classes/Units.js index 89c4bd1b6..0885cc2c7 100644 --- a/app/static/app/js/classes/Units.js +++ b/app/static/app/js/classes/Units.js @@ -177,6 +177,7 @@ class UnitSystem{ volumeUnit(cbmeters, opts = {}){ throw new Error("Not implemented"); } getName(){ throw new Error("Not implemented"); } + getKey(){ throw new Error("Not implemented"); } area(sqmeters, opts = {}){ sqmeters = parseFloat(sqmeters); @@ -187,6 +188,10 @@ class UnitSystem{ return new ValueUnit(val, unit); } + elevation(meters){ + return this.length(meters, { fixedUnit: true }); + } + length(meters, opts = {}){ meters = parseFloat(meters); if (isNaN(meters)) return NanUnit(); @@ -233,6 +238,10 @@ class MetricSystem extends UnitSystem{ return _("Metric"); } + getKey(){ + return "metric"; + } + lengthUnit(meters, opts = {}){ if (opts.fixedUnit) return units.meters; @@ -259,6 +268,10 @@ class ImperialSystem extends UnitSystem{ return _("Imperial"); } + getKey(){ + return "imperial"; + } + feet(){ return units.feet; } @@ -310,6 +323,10 @@ class ImperialUSSystem extends ImperialSystem{ return _("Imperial (US)"); } + getKey(){ + return "imperialUS"; + } + feet(){ return units.feet_us; } diff --git a/app/static/app/js/classes/Utils.js b/app/static/app/js/classes/Utils.js index cb449647b..7df79cde5 100644 --- a/app/static/app/js/classes/Utils.js +++ b/app/static/app/js/classes/Utils.js @@ -107,6 +107,18 @@ export default { isMobile: function(){ return navigator.userAgent.match(/(iPad)|(iPhone)|(iPod)|(android)|(webOS)/i); + }, + + userInputToFilename(text, extension = ""){ + return text.replace(/[^0-9a-zA-Z-_]+/g, '').replace(/(\s|\/)+/g, "-") + extension; + }, + + uuidv4(){ + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + var r = Math.random() * 16 | 0, + v = c === 'x' ? r : r & 0x3 | 0x8; + return v.toString(16); + }); } }; diff --git a/app/static/app/js/classes/plugins/Map.js b/app/static/app/js/classes/plugins/Map.js index f7aae890f..6575093cc 100644 --- a/app/static/app/js/classes/plugins/Map.js +++ b/app/static/app/js/classes/plugins/Map.js @@ -23,7 +23,13 @@ export default { ], functions: [ - "handleClick" + "handleClick", + "addAnnotation", + "updateAnnotation", + "deleteAnnotation", + "toggleAnnotation", + "annotationDeleted", + "downloadAnnotations" ] }; diff --git a/app/static/app/js/components/LayersControl.jsx b/app/static/app/js/components/LayersControl.jsx index b82542d90..88dd88501 100644 --- a/app/static/app/js/components/LayersControl.jsx +++ b/app/static/app/js/components/LayersControl.jsx @@ -9,6 +9,7 @@ class LayersControlButton extends React.Component { static propTypes = { layers: PropTypes.array.isRequired, overlays: PropTypes.array.isRequired, + annotations: PropTypes.array.isRequired, map: PropTypes.object.isRequired } @@ -36,7 +37,7 @@ class LayersControlButton extends React.Component { title="Layers" onClick={this.handleOpen} className="leaflet-control-layers-control-button leaflet-bar-part theme-secondary"> - + ); } } @@ -51,13 +52,13 @@ export default L.Control.extend({ this.map = map; L.DomEvent.disableClickPropagation(this.container); - this.update(this.options.layers, []); + this.update(this.options.layers, [], []); return this.container; }, - update: function(layers, overlays){ - ReactDOM.render(, this.container); + update: function(layers, overlays, annotations){ + ReactDOM.render(, this.container); } }); diff --git a/app/static/app/js/components/LayersControlAnnotations.jsx b/app/static/app/js/components/LayersControlAnnotations.jsx new file mode 100644 index 000000000..3d0bc6018 --- /dev/null +++ b/app/static/app/js/components/LayersControlAnnotations.jsx @@ -0,0 +1,158 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import '../css/LayersControlAnnotations.scss'; +import PluginsAPI from '../classes/plugins/API'; +import { Checkbox, ExpandButton } from './Toggle'; +import { _ } from '../classes/gettext'; + +class AnnotationLayer extends React.Component{ + static propTypes = { + parent: PropTypes.object, + layer: PropTypes.object + } + + constructor(props){ + super(props); + + this.state = { + visible: !!props.layer._map + } + } + + componentDidUpdate(prevProps, prevState){ + if (prevState.visible !== this.state.visible && this.props.parent.state.visible){ + PluginsAPI.Map.toggleAnnotation(this.props.layer, this.state.visible); + } + } + + componentDidMount(){ + PluginsAPI.Map.onUpdateAnnotation(this.handleUpdate); + } + + componentWillUnmount(){ + PluginsAPI.Map.offUpdateAnnotation(this.handleUpdate); + } + + handleUpdate = (layer, name) => { + if (this.props.layer === layer){ + const meta = layer[Symbol.for("meta")]; + meta.name = name; + this.forceUpdate(); + } + } + + handleFocus = () => { + const { layer } = this.props; + if (!layer._map) return; + + if (layer.options.bounds || layer.getBounds){ + const bounds = layer.options.bounds !== undefined ? + layer.options.bounds : + layer.getBounds(); + layer._map.fitBounds(bounds); + }else if (layer._latlng){ + layer._map.setView(layer._latlng, 22); + } + + // if (layer.getPopup()) layer.openPopup(); + } + + handleDelete = () => { + if (window.confirm(_('Are you sure you want to delete this?'))){ + PluginsAPI.Map.deleteAnnotation(this.props.layer); + } + } + + render(){ + const { layer } = this.props; + const meta = layer[Symbol.for("meta")]; + + return (
+
+
{meta.name}
+ +
+
); + } +} + +export default class LayersControlAnnotations extends React.Component { + static defaultProps = { + expanded: true, + visible: true, + layers: [] + }; + + static propTypes = { + expanded: PropTypes.bool, + visible: PropTypes.bool, + layers: PropTypes.array + } + + constructor(props){ + super(props); + + let visible = false; + for (let i = 0; i < props.layers.length; i++){ + if (props.layers[i]._map){ + visible = true; + break; + } + } + + this.state = { + visible, + expanded: props.expanded + }; + + this.annRefs = new Array(props.layers.length); + } + + handleAnnotationsClick = () => { + this.setState({expanded: !this.state.expanded}); + } + + componentDidUpdate(prevProps, prevState){ + if (prevState.visible !== this.state.visible){ + this.annRefs.forEach(ann => { + let visible = this.state.visible ? ann.state.visible : false; + PluginsAPI.Map.toggleAnnotation(ann.props.layer, visible); + }); + } + } + + handleExportGeoJSON = e => { + if (PluginsAPI.Map.downloadAnnotations("geojson")) return; + else{ + // TODO? + } + } + + handleDelete = () => { + if (window.confirm(_('Are you sure you want to delete this?'))){ + this.props.layers.forEach(layer => { + PluginsAPI.Map.deleteAnnotation(layer); + }); + } + } + + + render(){ + const { layers } = this.props; + + + return (
+ + +
+ {layers.map((layer, i) => this.annRefs[i] = domNode} key={i} layer={layer} />)} +
+
); + + } +} \ No newline at end of file diff --git a/app/static/app/js/components/LayersControlLayer.jsx b/app/static/app/js/components/LayersControlLayer.jsx index 15c710323..d76bb1bb3 100644 --- a/app/static/app/js/components/LayersControlLayer.jsx +++ b/app/static/app/js/components/LayersControlLayer.jsx @@ -109,7 +109,7 @@ export default class LayersControlLayer extends React.Component { } } - handleLayerClick = () => { + handleZoomToClick = () => { const { layer } = this.props; const bounds = layer.options.bounds !== undefined ? @@ -120,6 +120,14 @@ export default class LayersControlLayer extends React.Component { if (layer.getPopup()) layer.openPopup(); } + handleLayerClick = () => { + if (this.props.overlay){ + this.setState({visible: !this.state.visible}); + }else{ + this.setState({expanded: !this.state.expanded}); + } + } + handleSelectColor = e => { this.setState({colorMap: e.target.value}); } @@ -287,8 +295,10 @@ export default class LayersControlLayer extends React.Component { } return (
- {!this.props.overlay ? :
} - {meta.name} +
+ {!this.props.overlay ? :
} +
{meta.name}
+
{this.state.expanded ?
diff --git a/app/static/app/js/components/LayersControlPanel.jsx b/app/static/app/js/components/LayersControlPanel.jsx index cbd65bf70..b3112a854 100644 --- a/app/static/app/js/components/LayersControlPanel.jsx +++ b/app/static/app/js/components/LayersControlPanel.jsx @@ -2,17 +2,20 @@ import React from 'react'; import PropTypes from 'prop-types'; import '../css/LayersControlPanel.scss'; import LayersControlLayer from './LayersControlLayer'; +import LayersControlAnnotations from './LayersControlAnnotations'; import { _ } from '../classes/gettext'; export default class LayersControlPanel extends React.Component { static defaultProps = { layers: [], overlays: [], + annotations: [], }; static propTypes = { onClose: PropTypes.func.isRequired, layers: PropTypes.array.isRequired, overlays: PropTypes.array, + annotations: PropTypes.array, map: PropTypes.object.isRequired } @@ -25,17 +28,64 @@ export default class LayersControlPanel extends React.Component { if (!this.props.layers.length) content = ( {_("Loading…")}); else{ - content = (
- {this.props.overlays.length ? -
- {this.props.overlays.map((layer, i) => )} + // Check for grouping + const groups = {}; + const main = { + overlays: [], + layers: [], + annotations: [] + }; + + const scanGroup = destination => { + return l => { + const meta = l[Symbol.for("meta")]; + const group = meta.group; + if (group){ + groups[group.id] = groups[group.id] || { + overlays: [], + layers: [], + annotations: [], + name: group.name + }; + groups[group.id][destination].push(l); + }else{ + main[destination].push(l); + } + }; + }; + this.props.overlays.forEach(scanGroup('overlays')); + this.props.layers.forEach(scanGroup('layers')); + this.props.annotations.forEach(scanGroup('annotations')); + + const getGroupContent = group => { + return (
+ + {group.annotations.length ? +
+
- : ""} - {this.props.layers.sort((a, b) => { - const m_a = a[Symbol.for("meta")] || {}; - const m_b = b[Symbol.for("meta")] || {}; - return m_a.name > m_b.name ? -1 : 1; - }).map((layer, i) => )} + : ""} + + {group.overlays.length ? +
+ {group.overlays.map((layer, i) => )} +
+ : ""} + {group.layers.sort((a, b) => { + const m_a = a[Symbol.for("meta")] || {}; + const m_b = b[Symbol.for("meta")] || {}; + return m_a.name > m_b.name ? -1 : 1; + }).map((layer, i) => )} +
); + }; + + content = (
{getGroupContent(main)} + {Object.keys(groups).map(id => { + return (
+
{groups[id].name}
+ {getGroupContent(groups[id])} +
) + })}
); } diff --git a/app/static/app/js/components/Map.jsx b/app/static/app/js/components/Map.jsx index 945b5b0fd..3ae587343 100644 --- a/app/static/app/js/components/Map.jsx +++ b/app/static/app/js/components/Map.jsx @@ -35,7 +35,8 @@ class Map extends React.Component { showBackground: false, mapType: "orthophoto", public: false, - shareButtons: true + shareButtons: true, + permissions: ["view"] }; static propTypes = { @@ -43,7 +44,8 @@ class Map extends React.Component { tiles: PropTypes.array.isRequired, mapType: PropTypes.oneOf(['orthophoto', 'plant', 'dsm', 'dtm']), public: PropTypes.bool, - shareButtons: PropTypes.bool + shareButtons: PropTypes.bool, + permissions: PropTypes.array }; constructor(props) { @@ -56,13 +58,14 @@ class Map extends React.Component { showLoading: false, // for drag&drop of files and first load opacity: 100, imageryLayers: [], - overlays: [] + overlays: [], + annotations: [] }; this.basemaps = {}; this.mapBounds = null; this.autolayers = null; - this.addedCameraShots = false; + this.addedCameraShots = {}; this.loadImageryLayers = this.loadImageryLayers.bind(this); this.updatePopupFor = this.updatePopupFor.bind(this); @@ -94,6 +97,19 @@ class Map extends React.Component { return ""; } + typeToIcon = (type) => { + switch(type){ + case "orthophoto": + return "far fa-image fa-fw" + case "plant": + return "fa fa-seedling fa-fw"; + case "dsm": + case "dtm": + return "fa fa-chart-area fa-fw"; + } + return ""; +} + hasBands = (bands, orthophoto_bands) => { if (!orthophoto_bands) return false; @@ -216,10 +232,15 @@ class Map extends React.Component { }); // Associate metadata with this layer - meta.name = name + ` (${this.typeToHuman(type)})`; + meta.name = this.typeToHuman(type); + meta.icon = this.typeToIcon(type); meta.metaUrl = metaUrl; meta.unitForward = unitForward; meta.unitBackward = unitBackward; + if (this.props.tiles.length > 1){ + // Assign to a group + meta.group = {id: meta.task.id, name: meta.task.name}; + } layer[Symbol.for("meta")] = meta; layer[Symbol.for("tile-meta")] = mres; @@ -275,8 +296,7 @@ class Map extends React.Component { this.mapBounds = mapBounds; // Add camera shots layer if available - if (meta.task && meta.task.camera_shots && !this.addedCameraShots){ - + if (meta.task && meta.task.camera_shots && !this.addedCameraShots[meta.task.id]){ var camIcon = L.icon({ iconUrl: "/static/app/js/icons/marker-camera.png", iconSize: [41, 46], @@ -316,13 +336,17 @@ class Map extends React.Component { shotsLayer.addMarkers(markers, this.map); } }); - shotsLayer[Symbol.for("meta")] = {name: name + " " + _("(Cameras)"), icon: "fa fa-camera fa-fw"}; + shotsLayer[Symbol.for("meta")] = {name: _("Cameras"), icon: "fa fa-camera fa-fw"}; + if (this.props.tiles.length > 1){ + // Assign to a group + shotsLayer[Symbol.for("meta")].group = {id: meta.task.id, name: meta.task.name}; + } this.setState(update(this.state, { overlays: {$push: [shotsLayer]} })); - this.addedCameraShots = true; + this.addedCameraShots[meta.task.id] = true; } // Add ground control points layer if available @@ -366,7 +390,11 @@ class Map extends React.Component { gcpLayer.addMarkers(markers, this.map); } }); - gcpLayer[Symbol.for("meta")] = {name: name + " " + _("(GCPs)"), icon: "far fa-dot-circle fa-fw"}; + gcpLayer[Symbol.for("meta")] = {name: _("Ground Control Points"), icon: "far fa-dot-circle fa-fw"}; + if (this.props.tiles.length > 1){ + // Assign to a group + gcpLayer[Symbol.for("meta")].group = {id: meta.task.id, name: meta.task.name}; + } this.setState(update(this.state, { overlays: {$push: [gcpLayer]} @@ -405,6 +433,9 @@ class Map extends React.Component { // leaflet bug? $(this.container).addClass("leaflet-touch"); + PluginsAPI.Map.onAddAnnotation(this.handleAddAnnotation); + PluginsAPI.Map.onAnnotationDeleted(this.handleDeleteAnnotation); + PluginsAPI.Map.triggerWillAddControls({ map: this.map, tiles, @@ -476,7 +507,8 @@ _('Example:'), this.layersControl = new LayersControl({ layers: this.state.imageryLayers, - overlays: this.state.overlays + overlays: this.state.overlays, + annotations: this.state.annotations }).addTo(this.map); this.autolayers = Leaflet.control.autolayers({ @@ -614,6 +646,25 @@ _('Example:'), }); } + handleAddAnnotation = (layer, name, task) => { + const meta = { + name: name || "", + icon: "fa fa-sticky-note fa-fw" + }; + if (this.props.tiles.length > 1 && task){ + meta.group = {id: task.id, name: task.name}; + } + layer[Symbol.for("meta")] = meta; + + this.setState(update(this.state, { + annotations: {$push: [layer]} + })); + } + + handleDeleteAnnotation = (layer) => { + this.setState({annotations: this.state.annotations.filter(l => l !== layer)}); + } + componentDidUpdate(prevProps, prevState) { this.state.imageryLayers.forEach(imageryLayer => { imageryLayer.setOpacity(this.state.opacity / 100); @@ -625,8 +676,9 @@ _('Example:'), } if (this.layersControl && (prevState.imageryLayers !== this.state.imageryLayers || - prevState.overlays !== this.state.overlays)){ - this.layersControl.update(this.state.imageryLayers, this.state.overlays); + prevState.overlays !== this.state.overlays || + prevState.annotations !== this.state.annotations)){ + this.layersControl.update(this.state.imageryLayers, this.state.overlays, this.state.annotations); } } @@ -637,6 +689,10 @@ _('Example:'), this.tileJsonRequests.forEach(tileJsonRequest => tileJsonRequest.abort()); this.tileJsonRequests = []; } + + PluginsAPI.Map.offAddAnnotation(this.handleAddAnnotation); + PluginsAPI.Map.offAnnotationDeleted(this.handleAddAnnotation); + } handleMapMouseDown(e){ diff --git a/app/static/app/js/components/Toggle.jsx b/app/static/app/js/components/Toggle.jsx index 0dc493ee4..a4ce5baa6 100644 --- a/app/static/app/js/components/Toggle.jsx +++ b/app/static/app/js/components/Toggle.jsx @@ -4,6 +4,7 @@ import '../css/Toggle.scss'; class Toggle extends React.Component { static defaultProps = { + className: "" }; static propTypes = { bind: PropTypes.array.isRequired, // two element array, @@ -11,7 +12,8 @@ class Toggle extends React.Component { // and the second the boolean property to determine visibility // ex. [this, 'visible'] trueIcon: PropTypes.string, - falseIcon: PropTypes.string + falseIcon: PropTypes.string, + className: PropTypes.string, } constructor(props){ @@ -26,7 +28,7 @@ class Toggle extends React.Component { render(){ const [parent, prop] = this.props.bind; const icon = parent.state[prop] ? this.props.trueIcon: this.props.falseIcon; - return (); + return (); } } diff --git a/app/static/app/js/components/tests/LayersControlAnnotations.test.jsx b/app/static/app/js/components/tests/LayersControlAnnotations.test.jsx new file mode 100644 index 000000000..4df58e9ad --- /dev/null +++ b/app/static/app/js/components/tests/LayersControlAnnotations.test.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import LayersControlAnnotations from '../LayersControlAnnotations'; + +describe('', () => { + it('renders without exploding', () => { + const wrapper = mount(); + expect(wrapper.exists()).toBe(true); + }) +}); \ No newline at end of file diff --git a/app/static/app/js/css/LayersControlAnnotations.scss b/app/static/app/js/css/LayersControlAnnotations.scss new file mode 100644 index 000000000..eec3724f8 --- /dev/null +++ b/app/static/app/js/css/LayersControlAnnotations.scss @@ -0,0 +1,21 @@ +.annotation-toggle{ + margin-left: 3px; +} + +.layers-control-annotations{ + .layer-control-title{ + align-items: baseline; + margin-left: 32px; + } + + .layer-label{ + padding-left: 6px; + } + + + .annotation-name{ + display: inline; + position: relative; + top: -1px; + } +} \ No newline at end of file diff --git a/app/static/app/js/css/LayersControlLayer.scss b/app/static/app/js/css/LayersControlLayer.scss index 4cc0f3022..f9f59f432 100644 --- a/app/static/app/js/css/LayersControlLayer.scss +++ b/app/static/app/js/css/LayersControlLayer.scss @@ -1,25 +1,24 @@ .leaflet-touch .leaflet-control-layers-control, .leaflet-control-layers-control{ .layers-control-layer{ - clear: both; + .layer-control-title{ + display: flex; + flex-wrap: nowrap; + } .layer-label{ - width: auto; - position: relative; - top: 1px; + width: 100%; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; - display: block; text-align: left; - padding-left: 4px; - margin-left: 50px; - height: 22px; - line-height: 22px; - padding-top: 1px; + height: 28px; + margin-left: 1px; } - .layer-expanded{ - clear: both; + .layer-action{ + flex-basis: min-content; + padding-left: 6px; + padding-right: 6px; } select, input{ @@ -46,7 +45,6 @@ } .toggle{ - float: left; width: 22px; i{ font-size: 18px; @@ -57,15 +55,20 @@ } } - .overlayIcon{ - float: left; + .paddingSpace{ width: 22px; + height: 22px; + } - i{ - font-size: 16px; - margin-top: 4px; - margin-left: 1px; - } + i.layer-icon{ + font-size: 16px; + margin-left: 4px; + } + + .layer-title{ + display: inline; + position: relative; + top: -1px; } } } \ No newline at end of file diff --git a/app/static/app/js/css/LayersControlPanel.scss b/app/static/app/js/css/LayersControlPanel.scss index 1351fa978..7a263243a 100644 --- a/app/static/app/js/css/LayersControlPanel.scss +++ b/app/static/app/js/css/LayersControlPanel.scss @@ -45,4 +45,12 @@ *{ font-size: 12px; } + + .layer-group-title{ + margin-top: 2px; + margin-bottom: 2px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } } diff --git a/app/static/app/js/translations/odm_autogenerated.js b/app/static/app/js/translations/odm_autogenerated.js index c6bb26a78..1b966af1f 100644 --- a/app/static/app/js/translations/odm_autogenerated.js +++ b/app/static/app/js/translations/odm_autogenerated.js @@ -1,93 +1,93 @@ // Auto-generated with extract_odm_strings.py, do not edit! -_("Copy output results to this folder after processing."); +_("Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive morphological filter. Check the --dem* parameters for finer tuning. Default: %(default)s"); +_("Use a full 3D mesh to compute the orthophoto instead of a 2.5D mesh. This option is a bit faster and provides similar results in planar areas. Default: %(default)s"); +_("Path to the image geolocation file containing the camera center coordinates used for georeferencing. If you don't have values for yaw/pitch/roll you can set them to 0. The file needs to use the following format: EPSG: or <+proj definition>image_name geo_x geo_y geo_z [yaw (degrees)] [pitch (degrees)] [roll (degrees)] [horz accuracy (meters)] [vert accuracy (meters)]Default: %(default)s"); +_("DSM/DTM resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. Default: %(default)s"); +_("Automatically compute image masks using AI to remove the background. Experimental. Default: %(default)s"); +_("Rerun this stage only and stop. Can be one of: %(choices)s. Default: %(default)s"); +_("Export the georeferenced point cloud in LAS format. Default: %(default)s"); +_("Generate OGC 3D Tiles outputs. Default: %(default)s"); +_("Computes an euclidean raster map for each DEM. The map reports the distance from each cell to the nearest NODATA value (before any hole filling takes place). This can be useful to isolate the areas that have been filled. Default: %(default)s"); +_("Override the rolling shutter readout time for your camera sensor (in milliseconds), instead of using the rolling shutter readout database. Note that not all cameras are present in the database. Set to 0 to use the database value. Default: %(default)s"); +_("Specify the distance between camera shot locations and the outer edge of the boundary when computing the boundary with --auto-boundary. Set to 0 to automatically choose a value. Default: %(default)s"); +_("End processing at this stage. Can be one of: %(choices)s. Default: %(default)s"); +_("Generates a polygon around the cropping area that cuts the orthophoto around the edges of features. This polygon can be useful for stitching seamless mosaics with multiple overlapping orthophotos. Default: %(default)s"); +_("GeoJSON polygon limiting the area of the reconstruction. Can be specified either as path to a GeoJSON file or as a JSON string representing the contents of a GeoJSON file. Default: %(default)s"); +_("Simple Morphological Filter elevation threshold parameter (meters). Default: %(default)s"); +_("Rerun processing from this stage. Can be one of: %(choices)s. Default: %(default)s"); +_("Ignore Ground Sampling Distance (GSD).A memory and processor hungry change relative to the default behavior if set to true. Ordinarily, GSD estimates are used to cap the maximum resolution of image outputs and resizes images when necessary, resulting in faster processing and lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality. Never set --ignore-gsd to true unless you are positive you need it, and even then: do not use it. Default: %(default)s"); +_("Skip alignment of submodels in split-merge. Useful if GPS is good enough on very large datasets. Default: %(default)s"); _("The maximum number of processes to use in various processes. Peak memory requirement is ~1GB per thread and 2 megapixel image resolution. Default: %(default)s"); +_("Keep faces in the mesh that are not seen in any camera. Default: %(default)s"); +_("Generate OBJs that have a single material and a single texture file instead of multiple ones. Default: %(default)s"); +_("Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s"); +_("Radius of the overlap between submodels. After grouping images into clusters, images that are closer than this radius to a cluster are added to the cluster. This is done to ensure that neighboring submodels overlap. Default: %(default)s"); +_("Displays version number and exits. "); +_("Simple Morphological Filter window radius parameter (meters). Default: %(default)s"); +_("Automatically compute image masks using AI to remove the sky. Experimental. Default: %(default)s"); +_("Use this tag to build a DTM (Digital Terrain Model, ground only) using a simple morphological filter. Check the --dem* and --smrf* parameters for finer tuning. Default: %(default)s"); +_("Path to the image groups file that controls how images should be split into groups. The file needs to use the following format: image_name group_nameDefault: %(default)s"); +_("Use the camera parameters computed from another dataset instead of calculating them. Can be specified either as path to a cameras.json file or as a JSON string representing the contents of a cameras.json file. Default: %(default)s"); _("Build orthophoto overviews for faster display in programs such as QGIS. Default: %(default)s"); +_("Set the compression to use for orthophotos. Can be one of: %(choices)s. Default: %(default)s"); +_("Create Cloud-Optimized GeoTIFFs instead of normal GeoTIFFs. Default: %(default)s"); _("Classify the point cloud outputs. You can control the behavior of this option by tweaking the --dem-* parameters. Default: %(default)s"); -_("Path to a GeoTIFF DEM or a LAS/LAZ point cloud that the reconstruction outputs should be automatically aligned to. Experimental. Default: %(default)s"); +_("Turn on rolling shutter correction. If the camera has a rolling shutter and the images were taken in motion, you can turn on this option to improve the accuracy of the results. See also --rolling-shutter-readout. Default: %(default)s"); +_("Do not attempt to merge partial reconstructions. This can happen when images do not have sufficient overlap or are isolated. Default: %(default)s"); _("Skip generation of the orthophoto. This can save time if you only need 3D results or DEMs. Default: %(default)s"); -_("Automatically compute image masks using AI to remove the sky. Experimental. Default: %(default)s"); -_("Generate static tiles for orthophotos and DEMs that are suitable for viewers like Leaflet or OpenLayers. Default: %(default)s"); -_("Path to the project folder. Your project folder should contain subfolders for each dataset. Each dataset should have an \"images\" folder."); -_("Rerun processing from this stage. Can be one of: %(choices)s. Default: %(default)s"); -_("Decimate the points before generating the DEM. 1 is no decimation (full quality). 100 decimates ~99%% of the points. Useful for speeding up generation of DEM results in very large datasets. Default: %(default)s"); -_("Generates a polygon around the cropping area that cuts the orthophoto around the edges of features. This polygon can be useful for stitching seamless mosaics with multiple overlapping orthophotos. Default: %(default)s"); -_("Set a value in meters for the GPS Dilution of Precision (DOP) information for all images. If your images are tagged with high precision GPS information (RTK), this value will be automatically set accordingly. You can use this option to manually set it in case the reconstruction fails. Lowering this option can sometimes help control bowling-effects over large areas. Default: %(default)s"); -_("Keep faces in the mesh that are not seen in any camera. Default: %(default)s"); -_("Simple Morphological Filter slope parameter (rise over run). Default: %(default)s"); -_("Set point cloud quality. Higher quality generates better, denser point clouds, but requires more memory and takes longer. Each step up in quality increases processing time roughly by a factor of 4x.Can be one of: %(choices)s. Default: %(default)s"); -_("Set the compression to use for orthophotos. Can be one of: %(choices)s. Default: %(default)s"); -_("Use this tag to build a DTM (Digital Terrain Model, ground only) using a simple morphological filter. Check the --dem* and --smrf* parameters for finer tuning. Default: %(default)s"); -_("URL to a ClusterODM instance for distributing a split-merge workflow on multiple nodes in parallel. Default: %(default)s"); -_("Rerun this stage only and stop. Can be one of: %(choices)s. Default: %(default)s"); +_("Perform image matching with the nearest images based on GPS exif data. Set to 0 to match by triangulation. Default: %(default)s"); _("Save the georeferenced point cloud in Cloud Optimized Point Cloud (COPC) format. Default: %(default)s"); -_("Skip generation of a full 3D model. This can save time if you only need 2D results such as orthophotos and DEMs. Default: %(default)s"); -_("Ignore Ground Sampling Distance (GSD).A memory and processor hungry change relative to the default behavior if set to true. Ordinarily, GSD estimates are used to cap the maximum resolution of image outputs and resizes images when necessary, resulting in faster processing and lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality. Never set --ignore-gsd to true unless you are positive you need it, and even then: do not use it. Default: %(default)s"); -_("Generate single file Binary glTF (GLB) textured models. Default: %(default)s"); -_("Displays version number and exits. "); -_("Average number of images per submodel. When splitting a large dataset into smaller submodels, images are grouped into clusters. This value regulates the number of images that each cluster should have on average. Default: %(default)s"); +_("Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud and remove duplicate points. Set to 0 to disable sampling. Default: %(default)s"); _("Perform ground rectification on the point cloud. This means that wrongly classified ground points will be re-classified and gaps will be filled. Useful for generating DTMs. Default: %(default)s"); +_("Delete heavy intermediate files to optimize disk space usage. This affects the ability to restart the pipeline from an intermediate stage, but allows datasets to be processed on machines that don't have sufficient disk space available. Default: %(default)s"); +_("Name of dataset (i.e subfolder name within project folder). Default: %(default)s"); _("Automatically crop image outputs by creating a smooth buffer around the dataset boundaries, shrunk by N meters. Use 0 to disable cropping. Default: %(default)s"); -_("Use images' GPS exif data for reconstruction, even if there are GCPs present.This flag is useful if you have high precision GPS measurements. If there are no GCPs, this flag does nothing. Default: %(default)s"); -_("Export the georeferenced point cloud in LAS format. Default: %(default)s"); -_("Choose the structure from motion algorithm. For aerial datasets, if camera GPS positions and angles are available, triangulation can generate better results. For planar scenes captured at fixed altitude with nadir-only images, planar can be much faster. Can be one of: %(choices)s. Default: %(default)s"); -_("Skip generation of PDF report. This can save time if you don't need a report. Default: %(default)s"); _("Turn off camera parameter optimization during bundle adjustment. This can be sometimes useful for improving results that exhibit doming/bowling or when images are taken with a rolling shutter camera. Default: %(default)s"); -_("Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud and remove duplicate points. Set to 0 to disable sampling. Default: %(default)s"); -_("Name of dataset (i.e subfolder name within project folder). Default: %(default)s"); -_("Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive morphological filter. Check the --dem* parameters for finer tuning. Default: %(default)s"); -_("GeoJSON polygon limiting the area of the reconstruction. Can be specified either as path to a GeoJSON file or as a JSON string representing the contents of a GeoJSON file. Default: %(default)s"); +_("Skip generation of a full 3D model. This can save time if you only need 2D results such as orthophotos and DEMs. Default: %(default)s"); _("When processing multispectral datasets, you can specify the name of the primary band that will be used for reconstruction. It's recommended to choose a band which has sharp details and is in focus. Default: %(default)s"); -_("Path to the image geolocation file containing the camera center coordinates used for georeferencing. If you don't have values for yaw/pitch/roll you can set them to 0. The file needs to use the following format: EPSG: or <+proj definition>image_name geo_x geo_y geo_z [yaw (degrees)] [pitch (degrees)] [roll (degrees)] [horz accuracy (meters)] [vert accuracy (meters)]Default: %(default)s"); -_("Do not use GPU acceleration, even if it's available. Default: %(default)s"); -_("Geometric estimates improve the accuracy of the point cloud by computing geometrically consistent depthmaps but may not be usable in larger datasets. This flag disables geometric estimates. Default: %(default)s"); -_("Filters the point cloud by removing points that deviate more than N standard deviations from the local mean. Set to 0 to disable filtering. Default: %(default)s"); -_("Set the radiometric calibration to perform on images. When processing multispectral and thermal images you should set this option to obtain reflectance/temperature values (otherwise you will get digital number values). [camera] applies black level, vignetting, row gradient gain/exposure compensation (if appropriate EXIF tags are found) and computes absolute temperature values. [camera+sun] is experimental, applies all the corrections of [camera], plus compensates for spectral radiance registered via a downwelling light sensor (DLS) taking in consideration the angle of the sun. Can be one of: %(choices)s. Default: %(default)s"); +_("show this help message and exit"); +_("Copy output results to this folder after processing."); +_("Path to a GeoTIFF DEM or a LAS/LAZ point cloud that the reconstruction outputs should be automatically aligned to. Experimental. Default: %(default)s"); +_("Skip generation of PDF report. This can save time if you don't need a report. Default: %(default)s"); _("Octree depth used in the mesh reconstruction, increase to get more vertices, recommended values are 8-12. Default: %(default)s"); -_("Generate OGC 3D Tiles outputs. Default: %(default)s"); +_("Path to the file containing the ground control points used for georeferencing. The file needs to use the following format: EPSG: or <+proj definition>geo_x geo_y geo_z im_x im_y image_name [gcp_name] [extra1] [extra2]Default: %(default)s"); +_("URL to a ClusterODM instance for distributing a split-merge workflow on multiple nodes in parallel. Default: %(default)s"); +_("Simple Morphological Filter elevation scalar parameter. Default: %(default)s"); +_("Permanently delete all previous results and rerun the processing pipeline."); +_("Matcher algorithm, Fast Library for Approximate Nearest Neighbors or Bag of Words. FLANN is slower, but more stable. BOW is faster, but can sometimes miss valid matches. BRUTEFORCE is very slow but robust.Can be one of: %(choices)s. Default: %(default)s"); +_("Decimate the points before generating the DEM. 1 is no decimation (full quality). 100 decimates ~99%% of the points. Useful for speeding up generation of DEM results in very large datasets. Default: %(default)s"); +_("Maximum number of frames to extract from video files for processing. Set to 0 for no limit. Default: %(default)s"); +_("Use this tag if you have a GCP File but want to use the EXIF information for georeferencing instead. Default: %(default)s"); +_("Choose what to merge in the merge step in a split dataset. By default all available outputs are merged. Options: %(choices)s. Default: %(default)s"); +_("Filters the point cloud by removing points that deviate more than N standard deviations from the local mean. Set to 0 to disable filtering. Default: %(default)s"); +_("Simple Morphological Filter slope parameter (rise over run). Default: %(default)s"); +_("The maximum vertex count of the output mesh. Default: %(default)s"); +_("Path to the project folder. Your project folder should contain subfolders for each dataset. Each dataset should have an \"images\" folder."); _("Minimum number of features to extract per image. More features can be useful for finding more matches between images, potentially allowing the reconstruction of areas with little overlap or insufficient features. More features also slow down processing. Default: %(default)s"); _("Run local bundle adjustment for every image added to the reconstruction and a global adjustment every 100 images. Speeds up reconstruction for very large datasets. Default: %(default)s"); -_("Skips dense reconstruction and 3D model generation. It generates an orthophoto directly from the sparse reconstruction. If you just need an orthophoto and do not need a full 3D model, turn on this option. Default: %(default)s"); -_("Create Cloud-Optimized GeoTIFFs instead of normal GeoTIFFs. Default: %(default)s"); -_("Choose what to merge in the merge step in a split dataset. By default all available outputs are merged. Options: %(choices)s. Default: %(default)s"); -_("Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s"); -_("Delete heavy intermediate files to optimize disk space usage. This affects the ability to restart the pipeline from an intermediate stage, but allows datasets to be processed on machines that don't have sufficient disk space available. Default: %(default)s"); -_("Matcher algorithm, Fast Library for Approximate Nearest Neighbors or Bag of Words. FLANN is slower, but more stable. BOW is faster, but can sometimes miss valid matches. BRUTEFORCE is very slow but robust.Can be one of: %(choices)s. Default: %(default)s"); -_("Automatically set a boundary using camera shot locations to limit the area of the reconstruction. This can help remove far away background artifacts (sky, background landscapes, etc.). See also --boundary. Default: %(default)s"); +_("Set point cloud quality. Higher quality generates better, denser point clouds, but requires more memory and takes longer. Each step up in quality increases processing time roughly by a factor of 4x.Can be one of: %(choices)s. Default: %(default)s"); +_("Do not use GPU acceleration, even if it's available. Default: %(default)s"); +_("Set feature extraction quality. Higher quality generates better features, but requires more memory and takes longer. Can be one of: %(choices)s. Default: %(default)s"); +_("The maximum output resolution of extracted video frames in pixels. Default: %(default)s"); +_("Average number of images per submodel. When splitting a large dataset into smaller submodels, images are grouped into clusters. This value regulates the number of images that each cluster should have on average. Default: %(default)s"); +_("Choose the structure from motion algorithm. For aerial datasets, if camera GPS positions and angles are available, triangulation can generate better results. For planar scenes captured at fixed altitude with nadir-only images, planar can be much faster. Can be one of: %(choices)s. Default: %(default)s"); _("Skip normalization of colors across all images. Useful when processing radiometric data. Default: %(default)s"); -_("Simple Morphological Filter window radius parameter (meters). Default: %(default)s"); -_("Turn on rolling shutter correction. If the camera has a rolling shutter and the images were taken in motion, you can turn on this option to improve the accuracy of the results. See also --rolling-shutter-readout. Default: %(default)s"); -_("When processing multispectral datasets, ODM will automatically align the images for each band. If the images have been postprocessed and are already aligned, use this option. Default: %(default)s"); -_("The maximum vertex count of the output mesh. Default: %(default)s"); -_("Permanently delete all previous results and rerun the processing pipeline."); +_("Perform image matching with the nearest N images based on image filename order. Can speed up processing of sequential images, such as those extracted from video. It is applied only on non-georeferenced datasets. Set to 0 to disable. Default: %(default)s"); +_("Set a camera projection type. Manually setting a value can help improve geometric undistortion. By default the application tries to determine a lens type from the images metadata. Can be one of: %(choices)s. Default: %(default)s"); +_("Set this parameter if you want to generate a PNG rendering of the orthophoto. Default: %(default)s"); +_("Use images' GPS exif data for reconstruction, even if there are GCPs present.This flag is useful if you have high precision GPS measurements. If there are no GCPs, this flag does nothing. Default: %(default)s"); +_("Set the radiometric calibration to perform on images. When processing multispectral and thermal images you should set this option to obtain reflectance/temperature values (otherwise you will get digital number values). [camera] applies black level, vignetting, row gradient gain/exposure compensation (if appropriate EXIF tags are found) and computes absolute temperature values. [camera+sun] is experimental, applies all the corrections of [camera], plus compensates for spectral radiance registered via a downwelling light sensor (DLS) taking in consideration the angle of the sun. Can be one of: %(choices)s. Default: %(default)s"); _("Export the georeferenced point cloud in CSV format. Default: %(default)s"); -_("Simple Morphological Filter elevation threshold parameter (meters). Default: %(default)s"); -_("Maximum number of frames to extract from video files for processing. Set to 0 for no limit. Default: %(default)s"); -_("Specify the distance between camera shot locations and the outer edge of the boundary when computing the boundary with --auto-boundary. Set to 0 to automatically choose a value. Default: %(default)s"); +_("Set this parameter if you want a striped GeoTIFF. Default: %(default)s"); +_("Generate static tiles for orthophotos and DEMs that are suitable for viewers like Leaflet or OpenLayers. Default: %(default)s"); +_("Automatically set a boundary using camera shot locations to limit the area of the reconstruction. This can help remove far away background artifacts (sky, background landscapes, etc.). See also --boundary. Default: %(default)s"); +_("Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. Starting with a radius equal to the output resolution, N different DEMs are generated with progressively bigger radius using the inverse distance weighted (IDW) algorithm and merged together. Remaining gaps are then merged using nearest neighbor interpolation. Default: %(default)s"); _("Set this parameter if you want to generate a Google Earth (KMZ) rendering of the orthophoto. Default: %(default)s"); -_("Path to the image groups file that controls how images should be split into groups. The file needs to use the following format: image_name group_nameDefault: %(default)s"); -_("Radius of the overlap between submodels. After grouping images into clusters, images that are closer than this radius to a cluster are added to the cluster. This is done to ensure that neighboring submodels overlap. Default: %(default)s"); -_("Override the rolling shutter readout time for your camera sensor (in milliseconds), instead of using the rolling shutter readout database. Note that not all cameras are present in the database. Set to 0 to use the database value. Default: %(default)s"); +_("Geometric estimates improve the accuracy of the point cloud by computing geometrically consistent depthmaps but may not be usable in larger datasets. This flag disables geometric estimates. Default: %(default)s"); +_("Set a value in meters for the GPS Dilution of Precision (DOP) information for all images. If your images are tagged with high precision GPS information (RTK), this value will be automatically set accordingly. You can use this option to manually set it in case the reconstruction fails. Lowering this option can sometimes help control bowling-effects over large areas. Default: %(default)s"); +_("When processing multispectral datasets, ODM will automatically align the images for each band. If the images have been postprocessed and are already aligned, use this option. Default: %(default)s"); _("Choose the algorithm for extracting keypoints and computing descriptors. Can be one of: %(choices)s. Default: %(default)s"); -_("Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. Starting with a radius equal to the output resolution, N different DEMs are generated with progressively bigger radius using the inverse distance weighted (IDW) algorithm and merged together. Remaining gaps are then merged using nearest neighbor interpolation. Default: %(default)s"); -_("Automatically compute image masks using AI to remove the background. Experimental. Default: %(default)s"); -_("Set feature extraction quality. Higher quality generates better features, but requires more memory and takes longer. Can be one of: %(choices)s. Default: %(default)s"); -_("Simple Morphological Filter elevation scalar parameter. Default: %(default)s"); -_("Perform image matching with the nearest images based on GPS exif data. Set to 0 to match by triangulation. Default: %(default)s"); -_("Computes an euclidean raster map for each DEM. The map reports the distance from each cell to the nearest NODATA value (before any hole filling takes place). This can be useful to isolate the areas that have been filled. Default: %(default)s"); -_("Skip alignment of submodels in split-merge. Useful if GPS is good enough on very large datasets. Default: %(default)s"); -_("Path to the file containing the ground control points used for georeferencing. The file needs to use the following format: EPSG: or <+proj definition>geo_x geo_y geo_z im_x im_y image_name [gcp_name] [extra1] [extra2]Default: %(default)s"); -_("DSM/DTM resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. Default: %(default)s"); -_("End processing at this stage. Can be one of: %(choices)s. Default: %(default)s"); -_("Set this parameter if you want a striped GeoTIFF. Default: %(default)s"); -_("Set this parameter if you want to generate a PNG rendering of the orthophoto. Default: %(default)s"); -_("Generate OBJs that have a single material and a single texture file instead of multiple ones. Default: %(default)s"); -_("Do not attempt to merge partial reconstructions. This can happen when images do not have sufficient overlap or are isolated. Default: %(default)s"); -_("Use the camera parameters computed from another dataset instead of calculating them. Can be specified either as path to a cameras.json file or as a JSON string representing the contents of a cameras.json file. Default: %(default)s"); -_("show this help message and exit"); -_("Use this tag if you have a GCP File but want to use the EXIF information for georeferencing instead. Default: %(default)s"); -_("The maximum output resolution of extracted video frames in pixels. Default: %(default)s"); +_("Generate single file Binary glTF (GLB) textured models. Default: %(default)s"); _("Orthophoto resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate.Default: %(default)s"); -_("Perform image matching with the nearest N images based on image filename order. Can speed up processing of sequential images, such as those extracted from video. It is applied only on non-georeferenced datasets. Set to 0 to disable. Default: %(default)s"); -_("Use a full 3D mesh to compute the orthophoto instead of a 2.5D mesh. This option is a bit faster and provides similar results in planar areas. Default: %(default)s"); -_("Set a camera projection type. Manually setting a value can help improve geometric undistortion. By default the application tries to determine a lens type from the images metadata. Can be one of: %(choices)s. Default: %(default)s"); +_("Skips dense reconstruction and 3D model generation. It generates an orthophoto directly from the sparse reconstruction. If you just need an orthophoto and do not need a full 3D model, turn on this option. Default: %(default)s"); diff --git a/app/views/app.py b/app/views/app.py index bdf3fb5f6..4256234a0 100644 --- a/app/views/app.py +++ b/app/views/app.py @@ -13,6 +13,7 @@ from django.contrib.auth.decorators import login_required from django.utils.translation import ugettext as _ from django import forms +from app.views.utils import get_permissions from webodm import settings def index(request): @@ -73,7 +74,8 @@ def map(request, project_pk=None, task_pk=None): 'map-items': json.dumps(mapItems), 'title': title, 'public': 'false', - 'share-buttons': 'false' if settings.DESKTOP_MODE else 'true' + 'share-buttons': 'false' if settings.DESKTOP_MODE else 'true', + 'permissions': json.dumps(get_permissions(request.user, project)) }.items() }) diff --git a/app/views/public.py b/app/views/public.py index a5ecff02a..13bc5905b 100644 --- a/app/views/public.py +++ b/app/views/public.py @@ -7,6 +7,7 @@ from app.api.tasks import TaskSerializer from app.models import Task +from app.views.utils import get_permissions from django.views.decorators.csrf import ensure_csrf_cookie from webodm import settings @@ -31,6 +32,7 @@ def handle_map(request, template, task_pk=None, hide_title=False): 'public': 'true', 'share-buttons': 'false' if settings.DESKTOP_MODE else 'true', 'selected-map-type': request.GET.get('t', 'auto'), + 'permissions': json.dumps(get_permissions(request.user, task.project)) }.items() }) diff --git a/app/views/utils.py b/app/views/utils.py new file mode 100644 index 000000000..f21f416ed --- /dev/null +++ b/app/views/utils.py @@ -0,0 +1,6 @@ +def get_permissions(user, project): + perms = [] + for p in ["view", "change"]: + if user.has_perm('app.%s_project' % p, project): + perms.append(p) + return perms \ No newline at end of file diff --git a/coreplugins/contours/manifest.json b/coreplugins/contours/manifest.json index 3d71f0804..0b55b088d 100644 --- a/coreplugins/contours/manifest.json +++ b/coreplugins/contours/manifest.json @@ -1,8 +1,8 @@ { "name": "Contours", - "webodmMinVersion": "2.5.0", + "webodmMinVersion": "2.5.1", "description": "Compute, preview and export contours from DEMs", - "version": "1.1.0", + "version": "1.1.1", "author": "Piero Toffanin", "email": "pt@masseranolabs.com", "repository": "https://github.com/OpenDroneMap/WebODM", diff --git a/coreplugins/contours/public/ContoursPanel.jsx b/coreplugins/contours/public/ContoursPanel.jsx index e77f13099..d7ac54250 100644 --- a/coreplugins/contours/public/ContoursPanel.jsx +++ b/coreplugins/contours/public/ContoursPanel.jsx @@ -174,7 +174,7 @@ export default class ContoursPanel extends React.Component { this.setState({previewLayer: L.geoJSON(geojson, { onEachFeature: (feature, layer) => { if (feature.properties && feature.properties.level !== undefined) { - layer.bindPopup(`
${_("Elevation:")} ${us.length(feature.properties.level)}
`); + layer.bindPopup(`
${_("Elevation:")} ${us.elevation(feature.properties.level)}
`); } }, style: feature => { diff --git a/locale b/locale index cbb06f369..79a4f1a68 160000 --- a/locale +++ b/locale @@ -1 +1 @@ -Subproject commit cbb06f369b4538d2e4b43708bb4919620a897169 +Subproject commit 79a4f1a68b90368a5ec944abec1ec2c18d37f169 diff --git a/package.json b/package.json index 624a46ad7..6e43b3f38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "2.5.0", + "version": "2.5.1", "description": "User-friendly, extendable application and API for processing aerial imagery.", "main": "index.js", "scripts": {