Modified measure plugin to include volume calculation. Started working on GRASS engine
|
@ -0,0 +1,98 @@
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from string import Template
|
||||||
|
|
||||||
|
logger = logging.getLogger('app.logger')
|
||||||
|
|
||||||
|
class GrassEngine:
|
||||||
|
def __init__(self):
|
||||||
|
self.grass_binary = shutil.which('grass7') or \
|
||||||
|
shutil.which('grass72') or \
|
||||||
|
shutil.which('grass74')
|
||||||
|
|
||||||
|
if self.grass_binary is None:
|
||||||
|
logger.warning("Could not find a GRASS 7 executable. GRASS scripts will not work.")
|
||||||
|
else:
|
||||||
|
logger.info("Initializing GRASS engine using {}".format(self.grass_binary))
|
||||||
|
|
||||||
|
def create_context(self):
|
||||||
|
if self.grass_binary is None: raise GrassEngineException("GRASS engine is unavailable")
|
||||||
|
return GrassContext(self.grass_binary)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class GrassContext:
|
||||||
|
def __init__(self, grass_binary):
|
||||||
|
self.grass_binary = grass_binary
|
||||||
|
self.cwd = tempfile.mkdtemp('_webodm_grass')
|
||||||
|
self.template_args = {}
|
||||||
|
self.location = None
|
||||||
|
|
||||||
|
def add_file(self, filename, source, use_as_location=False):
|
||||||
|
param = os.path.splitext(filename)[0] # filename without extension
|
||||||
|
|
||||||
|
dst_path = os.path.abspath(os.path.join(self.cwd, filename))
|
||||||
|
with open(dst_path) as f:
|
||||||
|
f.write(source)
|
||||||
|
self.template_args[param] = dst_path
|
||||||
|
|
||||||
|
if use_as_location:
|
||||||
|
self.set_location(self.template_args[param])
|
||||||
|
|
||||||
|
return dst_path
|
||||||
|
|
||||||
|
def add_param(self, param, value):
|
||||||
|
self.template_args[param] = value
|
||||||
|
|
||||||
|
def set_location(self, location):
|
||||||
|
"""
|
||||||
|
:param location: either a "epsg:XXXXX" string or a path to a geospatial file defining the location
|
||||||
|
"""
|
||||||
|
if not location.startsWith('epsg:'):
|
||||||
|
location = os.path.abspath(location)
|
||||||
|
self.location = location
|
||||||
|
|
||||||
|
def execute(self, script):
|
||||||
|
"""
|
||||||
|
:param script: path to .grass script
|
||||||
|
:return: script output
|
||||||
|
"""
|
||||||
|
if self.location is None: raise GrassEngineException("Location is not set")
|
||||||
|
|
||||||
|
script = os.path.abspath(script)
|
||||||
|
|
||||||
|
# Create grass script via template substitution
|
||||||
|
try:
|
||||||
|
with open(script) as f:
|
||||||
|
script_content = f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise GrassEngineException("Script does not exist: {}".format(script))
|
||||||
|
|
||||||
|
tmpl = Template(script_content)
|
||||||
|
|
||||||
|
# Write script to disk
|
||||||
|
with open(os.path.join(self.cwd, 'script.sh')) as f:
|
||||||
|
f.write(tmpl.substitute(self.template_args))
|
||||||
|
|
||||||
|
# Execute it
|
||||||
|
p = subprocess.Popen([self.grass_binary, '-c', self.location, 'location', '--exec', 'sh', 'script.sh'],
|
||||||
|
cwd=self.cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
out, err = p.communicate()
|
||||||
|
|
||||||
|
if p.returncode == 0:
|
||||||
|
return out
|
||||||
|
else:
|
||||||
|
raise GrassEngineException("Could not execute GRASS script {} from {}: {}".format(script, self.cwd, err))
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
# Cleanup
|
||||||
|
if os.path.exists(self.cwd):
|
||||||
|
shutil.rmtree(self.cwd)
|
||||||
|
|
||||||
|
class GrassEngineException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
grass = GrassEngine()
|
|
@ -200,7 +200,7 @@ pre.prettyprint,
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Failed */
|
/* Failed */
|
||||||
.task-list-item .status-label.error{
|
.task-list-item .status-label.error, .theme-background-failed{
|
||||||
background-color: theme("failed");
|
background-color: theme("failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,9 @@ if (!window.PluginsAPI){
|
||||||
|
|
||||||
// Globals always available in the window object
|
// Globals always available in the window object
|
||||||
'jQuery': { loader: 'globals-loader', exports: '$' },
|
'jQuery': { loader: 'globals-loader', exports: '$' },
|
||||||
'leaflet': { loader: 'globals-loader', exports: 'L' }
|
'leaflet': { loader: 'globals-loader', exports: 'L' },
|
||||||
|
'ReactDOM': { loader: 'globals-loader', exports: 'ReactDOM' },
|
||||||
|
'React': { loader: 'globals-loader', exports: 'React' }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -41,11 +41,16 @@ export default class ApiFactory{
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = {};
|
let obj = {};
|
||||||
api.endpoints.forEach(endpoint => {
|
api.endpoints.forEach(endpoint => {
|
||||||
if (!Array.isArray(endpoint)) endpoint = [endpoint];
|
if (!Array.isArray(endpoint)) endpoint = [endpoint];
|
||||||
addEndpoint(obj, ...endpoint);
|
addEndpoint(obj, ...endpoint);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (api.helpers){
|
||||||
|
obj = Object.assign(obj, api.helpers);
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Utils from '../Utils';
|
import Utils from '../Utils';
|
||||||
|
import L from 'leaflet';
|
||||||
|
|
||||||
const { assert } = Utils;
|
const { assert } = Utils;
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-right .leaflet-control,
|
||||||
.leaflet-control-measure.leaflet-control{
|
.leaflet-control-measure.leaflet-control{
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle{
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-opacity-slider{
|
.popup-opacity-slider{
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import '../css/main.scss';
|
import '../css/main.scss';
|
||||||
import './django/csrf';
|
import './django/csrf';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import React from 'react';
|
||||||
import PluginsAPI from './classes/plugins/API';
|
import PluginsAPI from './classes/plugins/API';
|
||||||
|
|
||||||
// Main is always executed first in the page
|
// Main is always executed first in the page
|
||||||
|
|
||||||
// We share the ReactDOM object to avoid having to include it
|
// We share some objects to avoid having to include it
|
||||||
// as a dependency in each component (adds too much space overhead)
|
// as a dependency in each component (adds too much space overhead)
|
||||||
window.ReactDOM = ReactDOM;
|
window.ReactDOM = ReactDOM;
|
||||||
|
window.React = React;
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
"immutability-helper": "^2.0.0",
|
"immutability-helper": "^2.0.0",
|
||||||
"jest": "^21.0.1",
|
"jest": "^21.0.1",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"leaflet": "^1.0.1",
|
"leaflet": "^1.3.1",
|
||||||
"node-sass": "^3.10.1",
|
"node-sass": "^3.10.1",
|
||||||
"object.values": "^1.0.3",
|
"object.values": "^1.0.3",
|
||||||
"proj4": "^2.4.3",
|
"proj4": "^2.4.3",
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from app.api.tasks import TaskNestedView
|
||||||
|
|
||||||
|
from app.plugins.grass_engine import grass
|
||||||
|
|
||||||
|
|
||||||
|
class GeoJSONSerializer(serializers.Serializer):
|
||||||
|
area = serializers.JSONField(help_text="Polygon contour defining the volume area to compute")
|
||||||
|
|
||||||
|
|
||||||
|
class TaskVolume(TaskNestedView):
|
||||||
|
def post(self, request, pk=None):
|
||||||
|
task = self.get_and_check_task(request, pk)
|
||||||
|
if task.dsm_extent is None:
|
||||||
|
return Response({'error': 'No surface model available'})
|
||||||
|
|
||||||
|
serializer = GeoJSONSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
|
||||||
|
# context = grass.create_context()
|
||||||
|
# context.add_file('area_file.geojson', serializer['area'])
|
||||||
|
# context.add_file('points_file.geojson', 'aaa')
|
||||||
|
# context.add_param('dsm_file', os.path.abspath(task.get_asset_download_path("dsm.tif")))
|
||||||
|
# context.execute(os.path.join(
|
||||||
|
# os.path.dirname(os.path.abspath(__file__),
|
||||||
|
# "calc_volume.grass"
|
||||||
|
# )))
|
||||||
|
|
||||||
|
print(serializer['area'])
|
||||||
|
return Response(30, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# area_file: Geospatial file containing the area to measure
|
||||||
|
# points_file: Geospatial file containing the points defining the area
|
||||||
|
# dsm_file: GeoTIFF DEM containing the surface
|
||||||
|
# ------
|
||||||
|
# output: prints the volume to stdout
|
||||||
|
|
||||||
|
v.import input=${area_file} output=polygon_area --overwrite
|
||||||
|
v.import input=${points_file} output=polygon_points --overwrite
|
||||||
|
v.buffer -s --overwrite input=polygon_area type=area output=region distance=3 minordistance=3
|
||||||
|
g.region vector=region
|
||||||
|
|
||||||
|
r.import input=${dsm_file} output=dsm --overwrite
|
||||||
|
v.what.rast map=polygon_points raster=dsm column=height
|
||||||
|
v.to.rast input=polygon_area output=r_polygon_area use=val value=255 --overwrite
|
||||||
|
|
||||||
|
#v.surf.rst --overwrite input=polygon_points zcolumn=height elevation=dsm_below_pile mask=r_polygon_area
|
||||||
|
v.surf.bspline --overwrite input=polygon_points column=height raster_output=dsm_below_pile lambda_i=100
|
||||||
|
|
||||||
|
r.mapcalc expression='pile_height_above_dsm=dsm-dsm_below_pile' --overwrite
|
||||||
|
r.volume -f input=pile_height_above_dsm clump=r_polygon_area
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "Area/Length Measurements",
|
"name": "Volume/Area/Length Measurements",
|
||||||
"webodmMinVersion": "0.5.0",
|
"webodmMinVersion": "0.5.0",
|
||||||
"description": "A plugin to compute area and length measurements on Leaflet",
|
"description": "A plugin to compute volume, area and length measurements on Leaflet",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"author": "Piero Toffanin",
|
"author": "Abdelkoddouss Izem, Piero Toffanin",
|
||||||
"email": "pt@masseranolabs.com",
|
"email": "pt@masseranolabs.com",
|
||||||
"repository": "https://github.com/OpenDroneMap/WebODM",
|
"repository": "https://github.com/OpenDroneMap/WebODM",
|
||||||
"tags": ["area", "length", "measurements"],
|
"tags": ["volume", "area", "length", "measurements"],
|
||||||
"homepage": "https://github.com/OpenDroneMap/WebODM",
|
"homepage": "https://github.com/OpenDroneMap/WebODM",
|
||||||
"experimental": false,
|
"experimental": true,
|
||||||
"deprecated": false
|
"deprecated": false
|
||||||
}
|
}
|
|
@ -1,5 +1,33 @@
|
||||||
|
from app.plugins import MountPoint
|
||||||
from app.plugins import PluginBase
|
from app.plugins import PluginBase
|
||||||
|
from .api import TaskVolume
|
||||||
|
|
||||||
class Plugin(PluginBase):
|
class Plugin(PluginBase):
|
||||||
def include_js_files(self):
|
def include_js_files(self):
|
||||||
return ['main.js']
|
return ['main.js']
|
||||||
|
|
||||||
|
def api_mount_points(self):
|
||||||
|
return [
|
||||||
|
MountPoint('task/(?P<pk>[^/.]+)/volume', TaskVolume.as_view())
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# def get_volume(self, geojson):
|
||||||
|
# try:
|
||||||
|
# raster_path= self.assets_path("odm_dem", "dsm.tif")
|
||||||
|
# raster=gdal.Open(raster_path)
|
||||||
|
# gt=raster.GetGeoTransform()
|
||||||
|
# rb=raster.GetRasterBand(1)
|
||||||
|
# gdal.UseExceptions()
|
||||||
|
# geosom = reprojson(geojson, raster)
|
||||||
|
# coords=[(entry[0],entry[1]) for entry in rings(raster_path, geosom)]
|
||||||
|
# GSD=gt[1]
|
||||||
|
# volume=0
|
||||||
|
# print(rings(raster_path, geosom))
|
||||||
|
# print(GSD)
|
||||||
|
# med=statistics.median(entry[2] for entry in rings(raster_path, geosom))
|
||||||
|
# clip=clip_raster(raster_path, geosom, gt=None, nodata=-9999)
|
||||||
|
# return ((clip-med)*GSD*GSD)[clip!=-9999.0].sum()
|
||||||
|
#
|
||||||
|
# except FileNotFoundError as e:
|
||||||
|
# logger.warning(e)
|
|
@ -0,0 +1,102 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './MeasurePopup.scss';
|
||||||
|
import $ from 'jquery';
|
||||||
|
import L from 'leaflet';
|
||||||
|
|
||||||
|
module.exports = class MeasurePopup extends React.Component {
|
||||||
|
static defaultProps = {
|
||||||
|
map: {},
|
||||||
|
model: {},
|
||||||
|
resultFeature: {}
|
||||||
|
};
|
||||||
|
static propTypes = {
|
||||||
|
map: PropTypes.object.isRequired,
|
||||||
|
model: PropTypes.object.isRequired,
|
||||||
|
resultFeature: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
console.log(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
volume: null, // to be calculated,
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
this.calculateVolume();
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateVolume(){
|
||||||
|
const { lastCoord } = this.props.model;
|
||||||
|
let layers = this.getLayersAtCoords(L.latLng(
|
||||||
|
lastCoord.dd.y,
|
||||||
|
lastCoord.dd.x
|
||||||
|
));
|
||||||
|
|
||||||
|
// Did we select a layer?
|
||||||
|
if (layers.length > 0){
|
||||||
|
const layer = layers[layers.length - 1];
|
||||||
|
const meta = layer[Symbol.for("meta")];
|
||||||
|
if (meta){
|
||||||
|
const task = meta.task;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: `/api/plugins/measure/task/${task.id}/volume`,
|
||||||
|
data: JSON.stringify({'area': this.props.resultFeature.toGeoJSON()}),
|
||||||
|
contentType: "application/json"
|
||||||
|
}).done(result => {
|
||||||
|
if (result.volume){
|
||||||
|
this.setState({volume});
|
||||||
|
}else if (result.error){
|
||||||
|
this.setState({error: result.error});
|
||||||
|
}else{
|
||||||
|
this.setState({error: "Invalid response: " + result});
|
||||||
|
}
|
||||||
|
}).fail(error => {
|
||||||
|
this.setState({error});
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
console.warn("Cannot find [meta] symbol for layer: ", layer);
|
||||||
|
this.setState({volume: false});
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
this.setState({volume: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @return the layers in the map
|
||||||
|
// at a specific lat/lon
|
||||||
|
getLayersAtCoords(latlng){
|
||||||
|
const targetBounds = L.latLngBounds(latlng, latlng);
|
||||||
|
|
||||||
|
const intersects = [];
|
||||||
|
for (let l in this.props.map._layers){
|
||||||
|
const layer = this.props.map._layers[l];
|
||||||
|
|
||||||
|
if (layer.options && layer.options.bounds){
|
||||||
|
if (targetBounds.intersects(layer.options.bounds)){
|
||||||
|
intersects.push(layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersects;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
const { volume, error } = this.state;
|
||||||
|
|
||||||
|
return (<div className="plugin-measure popup">
|
||||||
|
<p>Area: {this.props.model.areaDisplay}</p>
|
||||||
|
<p>Perimeter: {this.props.model.lengthDisplay}</p>
|
||||||
|
{volume === null && !error && <p>Volume: <i>computing...</i> <i className="fa fa-cog fa-spin fa-fw" /></p>}
|
||||||
|
{typeof volume === "number" && <p>Volume: {volume.toFixed("3")} Cubic Meters</p>}
|
||||||
|
{error && <p>Volume: <span className="error theme-background-failed">{error}</span></p>}
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
.plugin-measure.popup{
|
||||||
|
p{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import L from 'leaflet';
|
||||||
|
import './app.scss';
|
||||||
|
import './dist/leaflet-measure';
|
||||||
|
import './dist/leaflet-measure.css';
|
||||||
|
import MeasurePopup from './MeasurePopup';
|
||||||
|
import ReactDOM from 'ReactDOM';
|
||||||
|
import React from 'react';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
module.exports = class App{
|
||||||
|
constructor(map){
|
||||||
|
this.map = map;
|
||||||
|
|
||||||
|
L.control.measure({
|
||||||
|
labels:{
|
||||||
|
measureDistancesAndAreas: 'Measure volume, area and length',
|
||||||
|
areaMeasurement: 'Measurement'
|
||||||
|
},
|
||||||
|
primaryLengthUnit: 'meters',
|
||||||
|
secondaryLengthUnit: 'feet',
|
||||||
|
primaryAreaUnit: 'sqmeters',
|
||||||
|
secondaryAreaUnit: 'acres'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
map.on('measurepopupshown', ({popupContainer, model, resultFeature}) => {
|
||||||
|
// Only modify area popup, length popup is fine as default
|
||||||
|
if (model.area !== 0){
|
||||||
|
const $container = $("<div/>"),
|
||||||
|
$popup = $(popupContainer);
|
||||||
|
|
||||||
|
$popup.children("p").empty();
|
||||||
|
$popup.children("h3:first-child").after($container);
|
||||||
|
|
||||||
|
ReactDOM.render(<MeasurePopup
|
||||||
|
model={model}
|
||||||
|
resultFeature={resultFeature}
|
||||||
|
map={map} />, $container.get(0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
.leaflet-control-measure,
|
||||||
|
.leaflet-measure-resultpopup{
|
||||||
|
h3{
|
||||||
|
font-size: 120%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-control-measure-interaction{
|
||||||
|
a{
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
line-height: auto !important;
|
||||||
|
display: initial !important;
|
||||||
|
|
||||||
|
&:hover{
|
||||||
|
background-color: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../leaflet-measure-ex/dist/
|
Przed Szerokość: | Wysokość: | Rozmiar: 397 B |
Przed Szerokość: | Wysokość: | Rozmiar: 762 B |
Przed Szerokość: | Wysokość: | Rozmiar: 387 B |
Przed Szerokość: | Wysokość: | Rozmiar: 692 B |
Przed Szerokość: | Wysokość: | Rozmiar: 326 B |
Przed Szerokość: | Wysokość: | Rozmiar: 462 B |
Przed Szerokość: | Wysokość: | Rozmiar: 192 B |
Przed Szerokość: | Wysokość: | Rozmiar: 277 B |
Przed Szerokość: | Wysokość: | Rozmiar: 491 B |
Przed Szerokość: | Wysokość: | Rozmiar: 1003 B |
Przed Szerokość: | Wysokość: | Rozmiar: 279 B |
Przed Szerokość: | Wysokość: | Rozmiar: 460 B |
|
@ -1 +0,0 @@
|
||||||
.leaflet-control-measure h3,.leaflet-measure-resultpopup h3{margin:0 0 12px 0;padding-bottom:10px;line-height:1em;font-weight:normal;font-size:1.1em;border-bottom:solid 1px #DDD}.leaflet-control-measure p,.leaflet-measure-resultpopup p{margin:10px 0 0;line-height:1em}.leaflet-control-measure p:first-child,.leaflet-measure-resultpopup p:first-child{margin-top:0}.leaflet-control-measure a,.leaflet-measure-resultpopup a{color:#5E66CC;text-decoration:none}.leaflet-control-measure a:hover,.leaflet-measure-resultpopup a:hover{opacity:0.5;text-decoration:none}.leaflet-control-measure .tasks,.leaflet-measure-resultpopup .tasks{margin:12px 0 0;padding:10px 0 0;border-top:solid 1px #DDD;list-style:none;list-style-image:none}.leaflet-control-measure .tasks li,.leaflet-measure-resultpopup .tasks li{display:inline;margin:0 10px 0 0}.leaflet-control-measure .tasks li:last-child,.leaflet-measure-resultpopup .tasks li:last-child{margin-right:0}.leaflet-control-measure .coorddivider,.leaflet-measure-resultpopup .coorddivider{color:#999}.leaflet-control-measure{background:#fff;border-radius:5px;box-shadow:0 1px 5px rgba(0,0,0,0.4)}.leaflet-control-measure .leaflet-control-measure-toggle,.leaflet-control-measure .leaflet-control-measure-toggle:hover{display:block;width:36px;height:36px;background-position:50% 50%;background-repeat:no-repeat;background-image:url(images/rulers.png);border-radius:5px;text-indent:100%;white-space:nowrap;overflow:hidden}.leaflet-retina .leaflet-control-measure .leaflet-control-measure-toggle,.leaflet-retina .leaflet-control-measure .leaflet-control-measure-toggle:hover{background-image:url(images/rulers_@2X.png);background-size:16px 16px}.leaflet-touch .leaflet-control-measure .leaflet-control-measure-toggle,.leaflet-touch .leaflet-control-measure .leaflet-control-measure-toggle:hover{width:44px;height:44px}.leaflet-control-measure .startprompt h3{margin-bottom:10px}.leaflet-control-measure .startprompt .tasks{margin-top:0;padding-top:0;border-top:0}.leaflet-control-measure .leaflet-control-measure-interaction{padding:10px 12px}.leaflet-control-measure .results .group{margin-top:10px;padding-top:10px;border-top:dotted 1px #eaeaea}.leaflet-control-measure .results .group:first-child{margin-top:0;padding-top:0;border-top:0}.leaflet-control-measure .results .heading{margin-right:5px;color:#999}.leaflet-control-measure a.start{padding-left:18px;background-repeat:no-repeat;background-position:0% 50%;background-image:url(images/start.png)}.leaflet-retina .leaflet-control-measure a.start{background-image:url(images/start_@2X.png);background-size:12px 12px}.leaflet-control-measure a.cancel{padding-left:18px;background-repeat:no-repeat;background-position:0% 50%;background-image:url(images/cancel.png)}.leaflet-retina .leaflet-control-measure a.cancel{background-image:url(images/cancel_@2X.png);background-size:12px 12px}.leaflet-control-measure a.finish{padding-left:18px;background-repeat:no-repeat;background-position:0% 50%;background-image:url(images/check.png)}.leaflet-retina .leaflet-control-measure a.finish{background-image:url(images/check_@2X.png);background-size:12px 12px}.leaflet-measure-resultpopup a.zoomto{padding-left:18px;background-repeat:no-repeat;background-position:0% 50%;background-image:url(images/focus.png)}.leaflet-retina .leaflet-measure-resultpopup a.zoomto{background-image:url(images/focus_@2X.png);background-size:12px 12px}.leaflet-measure-resultpopup a.deletemarkup{padding-left:18px;background-repeat:no-repeat;background-position:0% 50%;background-image:url(images/trash.png)}.leaflet-retina .leaflet-measure-resultpopup a.deletemarkup{background-image:url(images/trash_@2X.png);background-size:11px 12px}
|
|
|
@ -1,11 +1,6 @@
|
||||||
PluginsAPI.Map.willAddControls([
|
PluginsAPI.Map.willAddControls([
|
||||||
'measure/leaflet-measure.css',
|
'measure/build/app.js',
|
||||||
'measure/leaflet-measure.min.js'
|
'measure/build/app.css'
|
||||||
], function(options){
|
], function(options, App){
|
||||||
L.control.measure({
|
new App(options.map);
|
||||||
primaryLengthUnit: 'meters',
|
|
||||||
secondaryLengthUnit: 'feet',
|
|
||||||
primaryAreaUnit: 'sqmeters',
|
|
||||||
secondaryAreaUnit: 'acres'
|
|
||||||
}).addTo(options.map);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "volume",
|
"name": "measure",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
@ -8,7 +8,5 @@
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {}
|
||||||
"leaflet-draw": "^1.0.2"
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -69,6 +69,8 @@ module.exports = {
|
||||||
"jquery": "jQuery",
|
"jquery": "jQuery",
|
||||||
"SystemJS": "SystemJS",
|
"SystemJS": "SystemJS",
|
||||||
"PluginsAPI": "PluginsAPI",
|
"PluginsAPI": "PluginsAPI",
|
||||||
"leaflet": "leaflet"
|
"leaflet": "leaflet",
|
||||||
|
"ReactDOM": "ReactDOM",
|
||||||
|
"React": "React"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1 +0,0 @@
|
||||||
from .plugin import *
|
|
|
@ -1,21 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from app.api.tasks import TaskNestedView
|
|
||||||
|
|
||||||
|
|
||||||
class GeoJSONSerializer(serializers.Serializer):
|
|
||||||
geometry = serializers.JSONField(help_text="Polygon contour defining the volume area to compute")
|
|
||||||
|
|
||||||
|
|
||||||
class TaskVolume(TaskNestedView):
|
|
||||||
def post(self, request, pk=None):
|
|
||||||
task = self.get_and_check_task(request, pk)
|
|
||||||
serializer = GeoJSONSerializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
#result=task.get_volume(serializer.geometry)
|
|
||||||
return Response(serializer.geometry, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Volume Measurements",
|
|
||||||
"webodmMinVersion": "0.5.0",
|
|
||||||
"description": "A plugin to compute volume measurements from a DSM",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"author": "Abdelkoddouss Izem, Piero Toffanin",
|
|
||||||
"email": "pt@masseranolabs.com",
|
|
||||||
"repository": "https://github.com/OpenDroneMap/WebODM",
|
|
||||||
"tags": ["volume", "measurements"],
|
|
||||||
"homepage": "https://github.com/OpenDroneMap/WebODM",
|
|
||||||
"experimental": true,
|
|
||||||
"deprecated": false
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
from app.plugins import MountPoint
|
|
||||||
from app.plugins import PluginBase
|
|
||||||
from .api import TaskVolume
|
|
||||||
|
|
||||||
class Plugin(PluginBase):
|
|
||||||
def include_js_files(self):
|
|
||||||
return ['main.js']
|
|
||||||
|
|
||||||
def api_mount_points(self):
|
|
||||||
return [
|
|
||||||
MountPoint('task/(?P<pk>[^/.]+)/calculate$', TaskVolume.as_view())
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# def get_volume(self, geojson):
|
|
||||||
# try:
|
|
||||||
# raster_path= self.assets_path("odm_dem", "dsm.tif")
|
|
||||||
# raster=gdal.Open(raster_path)
|
|
||||||
# gt=raster.GetGeoTransform()
|
|
||||||
# rb=raster.GetRasterBand(1)
|
|
||||||
# gdal.UseExceptions()
|
|
||||||
# geosom = reprojson(geojson, raster)
|
|
||||||
# coords=[(entry[0],entry[1]) for entry in rings(raster_path, geosom)]
|
|
||||||
# GSD=gt[1]
|
|
||||||
# volume=0
|
|
||||||
# print(rings(raster_path, geosom))
|
|
||||||
# print(GSD)
|
|
||||||
# med=statistics.median(entry[2] for entry in rings(raster_path, geosom))
|
|
||||||
# clip=clip_raster(raster_path, geosom, gt=None, nodata=-9999)
|
|
||||||
# return ((clip-med)*GSD*GSD)[clip!=-9999.0].sum()
|
|
||||||
#
|
|
||||||
# except FileNotFoundError as e:
|
|
||||||
# logger.warning(e)
|
|
|
@ -1,92 +0,0 @@
|
||||||
import 'leaflet-draw';
|
|
||||||
import 'leaflet-draw/dist/leaflet.draw.css';
|
|
||||||
import $ from 'jquery';
|
|
||||||
import L from 'leaflet';
|
|
||||||
|
|
||||||
module.exports = class App{
|
|
||||||
constructor(map){
|
|
||||||
this.map = map;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupVolumeControls(){
|
|
||||||
const { map } = this;
|
|
||||||
|
|
||||||
const editableLayers = new L.FeatureGroup();
|
|
||||||
map.addLayer(editableLayers);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
position: 'topright',
|
|
||||||
draw: {
|
|
||||||
toolbar: {
|
|
||||||
buttons: {
|
|
||||||
polygon: 'Draw an awesome polygon'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
polyline: false,
|
|
||||||
polygon: {
|
|
||||||
showArea: true,
|
|
||||||
showLength: true,
|
|
||||||
|
|
||||||
allowIntersection: false, // Restricts shapes to simple polygons
|
|
||||||
drawError: {
|
|
||||||
color: '#e1e100', // Color the shape will turn when intersects
|
|
||||||
message: '<strong>Oh snap!<strong> Area cannot have intersections!' // Message that will show when intersect
|
|
||||||
},
|
|
||||||
shapeOptions: {
|
|
||||||
// color: '#bada55'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
circle: false,
|
|
||||||
rectangle: false,
|
|
||||||
marker: false,
|
|
||||||
circlemarker: false
|
|
||||||
},
|
|
||||||
edit: {
|
|
||||||
featureGroup: editableLayers,
|
|
||||||
// remove: false
|
|
||||||
edit: {
|
|
||||||
selectedPathOptions: {
|
|
||||||
maintainColor: true,
|
|
||||||
dashArray: '10, 10'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawControl = new L.Control.Draw(options);
|
|
||||||
map.addControl(drawControl);
|
|
||||||
|
|
||||||
// Is there a better way?
|
|
||||||
$(drawControl._container)
|
|
||||||
.find('a.leaflet-draw-draw-polygon')
|
|
||||||
.attr('title', 'Measure Volume');
|
|
||||||
|
|
||||||
map.on(L.Draw.Event.CREATED, (e) => {
|
|
||||||
const { layer } = e;
|
|
||||||
layer.feature = {geometry: {type: 'Polygon'} };
|
|
||||||
|
|
||||||
var paramList;
|
|
||||||
// $.ajax({
|
|
||||||
// type: 'POST',
|
|
||||||
// async: false,
|
|
||||||
// url: `/api/projects/${meta.task.project}/tasks/${meta.task.id}/volume`,
|
|
||||||
// data: JSON.stringify(e.layer.toGeoJSON()),
|
|
||||||
// contentType: "application/json",
|
|
||||||
// success: function (msg) {
|
|
||||||
// paramList = msg;
|
|
||||||
// },
|
|
||||||
// error: function (jqXHR, textStatus, errorThrown) {
|
|
||||||
// alert("get session failed " + errorThrown);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
e.layer.bindPopup('Volume: test');
|
|
||||||
|
|
||||||
editableLayers.addLayer(layer);
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on(L.Draw.Event.EDITED, (e) => {
|
|
||||||
console.log("EDITED ", e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
PluginsAPI.Map.willAddControls([
|
|
||||||
'volume/build/app.js',
|
|
||||||
'volume/build/app.css'
|
|
||||||
], function(options, App){
|
|
||||||
const app = new App(options.map);
|
|
||||||
app.setupVolumeControls();
|
|
||||||
});
|
|
|
@ -73,6 +73,7 @@ module.exports = {
|
||||||
// require("jquery") is external and available
|
// require("jquery") is external and available
|
||||||
// on the global let jQuery
|
// on the global let jQuery
|
||||||
"jquery": "jQuery",
|
"jquery": "jQuery",
|
||||||
"SystemJS": "SystemJS"
|
"SystemJS": "SystemJS",
|
||||||
|
"React": "React"
|
||||||
}
|
}
|
||||||
}
|
}
|