import React from 'react'; import PropTypes from 'prop-types'; import Storage from 'webodm/classes/Storage'; import L from 'leaflet'; require('leaflet.heat') import './ChangeDetectionPanel.scss'; import ErrorMessage from 'webodm/components/ErrorMessage'; import ReactTooltip from 'react-tooltip' export default class ChangeDetectionPanel extends React.Component { static defaultProps = { }; static propTypes = { onClose: PropTypes.func.isRequired, tasks: PropTypes.object.isRequired, isShowed: PropTypes.bool.isRequired, map: PropTypes.object.isRequired, alignSupported: PropTypes.bool.isRequired, } constructor(props){ super(props); this.state = { error: "", permanentError: "", epsg: Storage.getItem("last_changedetection_epsg") || "4326", customEpsg: Storage.getItem("last_changedetection_custom_epsg") || "4326", displayType: Storage.getItem("last_changedetection_display_type") || "contours", resolution: Storage.getItem("last_changedetection_resolution") || 0.2, minArea: Storage.getItem("last_changedetection_min_area") || 40, minHeight: Storage.getItem("last_changedetection_min_height") || 5, role: Storage.getItem("last_changedetection_role") || 'reference', align: this.props.alignSupported ? (Storage.getItem("last_changedetection_align") === 'true') : false, other: "", otherTasksInProject: new Map(), loading: true, task: props.tasks[0] || null, previewLoading: false, exportLoading: false, previewLayer: null, opacity: 100, }; } componentDidUpdate(){ if (this.props.isShowed && this.state.loading){ const {id: taskId, project} = this.state.task; this.loadingReq = $.getJSON(`/api/projects/${project}/tasks/`) .done(res => { const otherTasksInProject = new Map() if (!this.props.alignSupported) { const myTask = res.filter(({ id }) => id === taskId)[0] const { available_assets: myAssets } = myTask; const errors = [] if (myAssets.indexOf("dsm.tif") === -1) errors.push("No DSM is available. Make sure to process a task with either the --dsm option checked"); if (myAssets.indexOf("dtm.tif") === -1) errors.push("No DTM is available. Make sure to process a task with either the --dtm option checked"); if (errors.length > 0) { this.setState({permanentError: errors.join('\n')}); return } const otherTasksWithDEMs = res.filter(({ id }) => id !== taskId) .filter(({ available_assets }) => available_assets.indexOf("dsm.tif") >= 0 && available_assets.indexOf("dtm.tif") >= 0) if (otherTasksWithDEMs.length === 0) { this.setState({permanentError: "Couldn't find other tasks on the project. Please make sure there are other tasks on the project that have both a DTM and DSM."}); return } otherTasksWithDEMs.forEach(({ id, name }) => otherTasksInProject.set(id, name)) } else { res.filter(({ id }) => id !== taskId) .forEach(({ id, name }) => otherTasksInProject.set(id, name)) } if (otherTasksInProject.size === 0) { this.setState({permanentError: `Couldn't find other tasks on this project. This plugin must be used on projects with 2 or more tasks.`}) } else { const firstOtherTask = Array.from(otherTasksInProject.entries())[0][0] this.setState({otherTasksInProject, other: firstOtherTask}); } }) .fail(() => { this.setState({permanentError: `Cannot retrieve information for the current project. Are you are connected to the internet?`}) }) .always(() => { this.setState({loading: false}); this.loadingReq = null; }); } } componentWillUnmount(){ if (this.loadingReq){ this.loadingReq.abort(); this.loadingReq = null; } if (this.generateReq){ this.generateReq.abort(); this.generateReq = null; } } handleSelectMinArea = e => { this.setState({minArea: e.target.value}); } handleSelectResolution = e => { this.setState({resolution: e.target.value}); } handleSelectMinHeight = e => { this.setState({minHeight: e.target.value}); } handleSelectRole = e => { this.setState({role: e.target.value}); } handleSelectOther = e => { this.setState({other: e.target.value}); } handleSelectEpsg = e => { this.setState({epsg: e.target.value}); } handleSelectDisplayType = e => { this.setState({displayType: e.target.value}); } handleChangeAlign = e => { this.setState({align: e.target.checked}); } handleChangeCustomEpsg = e => { this.setState({customEpsg: e.target.value}); } getFormValues = () => { const { epsg, customEpsg, displayType, align, resolution, minHeight, minArea, other, role } = this.state; return { display_type: displayType, resolution: resolution, min_height: minHeight, min_area: minArea, role: role, epsg: epsg !== "custom" ? epsg : customEpsg, other_task: other, align: align, }; } waitForCompletion = (taskId, celery_task_id, cb) => { let errorCount = 0; const check = () => { $.ajax({ type: 'GET', url: `/api/plugins/changedetection/task/${taskId}/changedetection/check/${celery_task_id}` }).done(result => { if (result.error){ cb(result.error); }else if (result.ready){ cb(); }else{ // Retry setTimeout(() => check(), 2000); } }).fail(error => { console.warn(error); if (errorCount++ < 10) setTimeout(() => check(), 2000); else cb(JSON.stringify(error)); }); }; check(); } addPreview = (url, cb) => { const { map } = this.props; $.getJSON(url) .done((result) => { try{ this.removePreview(); if (result.max) { const heatMap = L.heatLayer(result.values, { max: result.max, radius: 9, minOpacity: 0 }) heatMap.setStyle = ({ opacity }) => heatMap.setOptions({ max: result.max / opacity } ) this.setState({ previewLayer: heatMap }); } else { let featureGroup = L.featureGroup(); result.features.forEach(feature => { const area = feature.properties.area.toFixed(2); const min = feature.properties.min.toFixed(2); const max = feature.properties.max.toFixed(2); const avg = feature.properties.avg.toFixed(2); const std = feature.properties.std.toFixed(2); let geojsonForLevel = L.geoJSON(feature) .bindPopup(`Area: ${area}m2
Min: ${min}m
Max: ${max}m
Avg: ${avg}m
Std: ${std}m`) featureGroup.addLayer(geojsonForLevel); }); featureGroup.geojson = result; this.setState({ previewLayer: featureGroup }); } this.state.previewLayer.addTo(map); cb(); }catch(e){ throw e cb(e.message); } }) .fail(cb); } removePreview = () => { const { map } = this.props; if (this.state.previewLayer){ map.removeLayer(this.state.previewLayer); this.setState({previewLayer: null}); } } generateChangeMap = (data, loadingProp, isPreview) => { this.setState({[loadingProp]: true, error: ""}); const taskId = this.state.task.id; // Save settings for next time Storage.setItem("last_changedetection_display_type", this.state.displayType); Storage.setItem("last_changedetection_resolution", this.state.resolution); Storage.setItem("last_changedetection_min_height", this.state.minHeight); Storage.setItem("last_changedetection_min_area", this.state.minArea); Storage.setItem("last_changedetection_epsg", this.state.epsg); Storage.setItem("last_changedetection_custom_epsg", this.state.customEpsg); Storage.setItem("last_changedetection_role", this.state.role); Storage.setItem("last_changedetection_align", this.state.align); this.generateReq = $.ajax({ type: 'POST', url: `/api/plugins/changedetection/task/${taskId}/changedetection/generate`, data: data }).done(result => { if (result.celery_task_id){ this.waitForCompletion(taskId, result.celery_task_id, error => { if (error) this.setState({[loadingProp]: false, 'error': error}); else{ const fileUrl = `/api/plugins/changedetection/task/${taskId}/changedetection/download/${result.celery_task_id}`; // Preview if (isPreview){ this.addPreview(fileUrl, e => { if (e) this.setState({error: JSON.stringify(e)}); this.setState({[loadingProp]: false}); }); }else{ // Download location.href = fileUrl; this.setState({[loadingProp]: false}); } } }); }else if (result.error){ this.setState({[loadingProp]: false, error: result.error}); }else{ this.setState({[loadingProp]: false, error: "Invalid response: " + result}); } }).fail(error => { this.setState({[loadingProp]: false, error: JSON.stringify(error)}); }); } handleExport = (format) => { return () => { const data = this.getFormValues(); data.format = format; data.display_type = 'contours' this.generateChangeMap(data, 'exportLoading', false); }; } handleShowPreview = () => { this.setState({previewLoading: true}); const data = this.getFormValues(); data.epsg = 4326; data.format = "GeoJSON"; this.generateChangeMap(data, 'previewLoading', true); } handleChangeOpacity = (evt) => { const opacity = parseFloat(evt.target.value) / 100; this.setState({opacity: opacity}); this.state.previewLayer.setStyle({ opacity: opacity }); this.props.map.closePopup(); } render(){ const { loading, task, otherTasksInProject, error, permanentError, other, epsg, customEpsg, exportLoading, minHeight, minArea, displayType, resolution, previewLoading, previewLayer, opacity, role, align } = this.state; const disabled = (epsg === "custom" && !customEpsg) || !other; let content = ""; if (loading) content = ( Loading...); else if (permanentError) content = (
{permanentError}
); else{ content = (

{this.props.alignSupported ?

:

}

{this.props.alignSupported ?

: ""}

meters/pixel

meters

sq meters

{epsg === "custom" ?
: ""} {previewLayer ?

: ""}
); } return (
Change Detection

{content}
); } }