import React from 'react'; import PropTypes from 'prop-types'; import Storage from 'webodm/classes/Storage'; import L from 'leaflet'; import area from '@turf/area' import './ElevationMapPanel.scss'; import ErrorMessage from 'webodm/components/ErrorMessage'; import ReactTooltip from 'react-tooltip' export default class ElevationMapPanel extends React.Component { static defaultProps = { }; static propTypes = { onClose: PropTypes.func.isRequired, tasks: PropTypes.object.isRequired, isShowed: PropTypes.bool.isRequired, map: PropTypes.object.isRequired, layersControl: PropTypes.object.isRequired } constructor(props){ super(props); this.state = { error: "", permanentError: "", interval: Storage.getItem("last_elevationmap_interval") || "5", reference: "Sea", noiseFilterSize: Storage.getItem("last_elevationmap_noise_filter_size") || "3", customNoiseFilterSize: Storage.getItem("last_elevationmap_custom_noise_filter_size") || "3", epsg: Storage.getItem("last_elevationmap_epsg") || "4326", customEpsg: Storage.getItem("last_elevationmap_custom_epsg") || "4326", references: [], 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, project} = this.state.task; this.loadingReq = $.getJSON(`/api/projects/${project}/tasks/${id}/`) .done(res => { const { available_assets } = res; let references = ['Sea']; if (available_assets.indexOf("dsm.tif") === -1) this.setState({permanentError: "No DSM is available. Make sure to process a task with either the --dsm option checked"}); if (available_assets.indexOf("dtm.tif") !== -1) references.push("Ground"); this.setState({references, reference: references[0]}); }) .fail(() => { this.setState({permanentError: `Cannot retrieve information for task ${id}. 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; } } handleSelectInterval = e => { this.setState({interval: e.target.value}); } handleSelectNoiseFilterSize = e => { this.setState({noiseFilterSize: e.target.value}); } handleChangeCustomNoiseFilterSize = e => { this.setState({customNoiseFilterSize: e.target.value}); } handleSelectReference = e => { this.setState({reference: e.target.value}); } handleChangeCustomInterval = e => { this.setState({customInterval: e.target.value}); } handleSelectEpsg = e => { this.setState({epsg: e.target.value}); } handleChangeCustomEpsg = e => { this.setState({customEpsg: e.target.value}); } getFormValues = () => { const { interval, customInterval, epsg, customEpsg, noiseFilterSize, customNoiseFilterSize, reference } = this.state; return { interval: interval !== "custom" ? interval : customInterval, epsg: epsg !== "custom" ? epsg : customEpsg, noise_filter_size: noiseFilterSize !== "custom" ? noiseFilterSize : customNoiseFilterSize, reference }; } waitForCompletion = (taskId, celery_task_id, cb) => { let errorCount = 0; const check = () => { $.ajax({ type: 'GET', url: `/api/plugins/elevationmap/task/${taskId}/elevationmap/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(); } heatmap_coloring = (value, lowest, highest) => { const ratio = (value - lowest) / (highest - lowest); const h = 315 * (1 - ratio) / 360; const s = 1; const l = 0.5; let r, g, b; const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); const toHex = x => { const hex = Math.round(x * 255).toString(16); return hex.length === 1 ? '0' + hex : hex; }; return `#${toHex(r)}${toHex(g)}${toHex(b)}`; } addGeoJSONFromURL = (url, cb) => { const { map, layersControl } = this.props; $.getJSON(url) .done((geojson) => { try{ this.removePreview(); // Calculating all the elevation levels present const allLevels = geojson.features.map(feature => [feature.properties.bottom, feature.properties.top]).flat().sort((a, b) => a - b); const lowestLevel = allLevels[0]; const highestLevel = allLevels[allLevels.length - 1]; let featureGroup = L.featureGroup(); geojson.features.forEach(levelFeature => { const top = levelFeature.properties.top; const bottom = levelFeature.properties.bottom; const rgbHex = this.heatmap_coloring((bottom + top) / 2, lowestLevel, highestLevel); const areaInLevel = area(levelFeature).toFixed(2); let geojsonForLevel = L.geoJSON(levelFeature).setStyle({color: rgbHex, fill: true, fillColor: rgbHex, fillOpacity: 1}) .bindPopup(`Altitude: Between ${bottom}m and ${top}m
Area: ${areaInLevel}m2`) .on('popupopen', popup => { // Make all other layers transparent and highlight the clicked one featureGroup.getLayers().forEach(layer => layer.setStyle({ fillOpacity: 0.4 * this.state.opacity})); popup.propagatedFrom.setStyle({ color: "black", fillOpacity: this.state.opacity }).bringToFront() }) .on('popupclose', popup => { // Reset all layers to their original state featureGroup.getLayers().forEach(layer => layer.bringToFront().setStyle({ fillOpacity: this.state.opacity })); popup.propagatedFrom.setStyle({ color: rgbHex }); }); featureGroup.addLayer(geojsonForLevel); }); featureGroup.geojson = geojson; this.setState({ previewLayer: featureGroup }); this.state.previewLayer.addTo(map); layersControl.addOverlay(this.state.previewLayer, "Elevation Map"); cb(); }catch(e){ cb(e.message); } }) .fail(cb); } removePreview = () => { const { map, layersControl } = this.props; if (this.state.previewLayer){ map.removeLayer(this.state.previewLayer); layersControl.removeLayer(this.state.previewLayer); this.setState({previewLayer: null}); } } generateElevationMap = (data, loadingProp, isPreview) => { this.setState({[loadingProp]: true, error: ""}); const taskId = this.state.task.id; // Save settings for next time Storage.setItem("last_elevationmap_interval", this.state.interval); Storage.setItem("last_elevationmap_custom_interval", this.state.customInterval); Storage.setItem("last_elevationmap_noise_filter_size", this.state.noiseFilterSize); Storage.setItem("last_elevationmap_custom_noise_filter_size", this.state.customNoiseFilterSize); Storage.setItem("last_elevationmap_epsg", this.state.epsg); Storage.setItem("last_elevationmap_custom_epsg", this.state.customEpsg); this.generateReq = $.ajax({ type: 'POST', url: `/api/plugins/elevationmap/task/${taskId}/elevationmap/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/elevationmap/task/${taskId}/elevationmap/download/${result.celery_task_id}`; // Preview if (isPreview){ this.addGeoJSONFromURL(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; this.generateElevationMap(data, 'exportLoading', false); }; } handleShowPreview = () => { this.setState({previewLoading: true}); const data = this.getFormValues(); data.epsg = 4326; data.format = "GeoJSON"; this.generateElevationMap(data, 'previewLoading', true); } handleChangeOpacity = (evt) => { const opacity = parseFloat(evt.target.value) / 100; this.setState({opacity: opacity}); this.state.previewLayer.setStyle({ opacity: opacity, fillOpacity: opacity }); this.props.map.closePopup(); } render(){ const { loading, task, references, error, permanentError, interval, reference, epsg, customEpsg, exportLoading, noiseFilterSize, customNoiseFilterSize, previewLoading, previewLayer, opacity} = this.state; const noiseFilterSizeValues = [{label: 'Do not filter noise', value: 0}, {label: 'Normal', value: 3}, {label: 'Aggressive', value: 5}]; const disabled = (epsg === "custom" && !customEpsg) || (noiseFilterSize === "custom" && !customNoiseFilterSize); let content = ""; if (loading) content = ( Loading...); else if (permanentError) content = (
{permanentError}
); else{ content = (

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

: ""}
); } return (
Elevation Map

{content}
); } }