diff --git a/package.json b/package.json index f4fc62d03376f21e44cad58a260620e6342c8e9f..ee8d434dc4f10302c49de6b9c95fe50fd49d55ae 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,11 @@ "dependencies": { "bpl": "git+https://7f8bbb4b0a383ad905fee5b0f7cbc0c22533b556:x-oauth-basic@github.com/Blocp/bpl.git", "dom-to-image": "^2.5.2", + "highcharts": "^5.0.9", + "highcharts-3d": "^0.1.2", "react": "^15.3.2", "react-dom": "^15.3.2", + "react-highcharts": "^11.5.0", "react-redux": "^4.4.5", "react-router": "^3.0.0", "react-router-redux": "^4.0.7", diff --git a/src/components/TurkHit/features.js b/src/components/TurkHit/features.js new file mode 100644 index 0000000000000000000000000000000000000000..aa8c5b830de210de979bf1cf316ea07aa5697eb5 --- /dev/null +++ b/src/components/TurkHit/features.js @@ -0,0 +1,9 @@ +const featuresDict = { + 1: 'Door', + 2: 'Window', + 3: 'Building Point', + 4: 'Roof Point', + +}; + +export default featuresDict; diff --git a/src/components/TurkHit/index.js b/src/components/TurkHit/index.js index 09416740f824e51b47485205372d7e90811cd556..27abed2d3a01f147321721dee14e78a3352a9c99 100644 --- a/src/components/TurkHit/index.js +++ b/src/components/TurkHit/index.js @@ -1,21 +1,270 @@ import React, { PropTypes, Component } from 'react'; +import Highcharts from 'highcharts'; +import ReactHighcharts from 'react-highcharts'; +import Highcharts3D from 'highcharts-3d'; import defaultForm from './defaultForm'; import './styles.css'; +import featuresDict from './features'; import turkHitPropTypes from '../../containers/Dimensions/propTypes'; import documentsPropType from '../../containers/Documents/propTypes'; import turkStatus from './turkStatus'; +// Need to call this to load the 3D highcharts module +Highcharts3D(ReactHighcharts.Highcharts); class TurkHit extends Component { constructor(props) { super(props); - // TODO not being used this.state = { - error: false, + displayChart: false, + chart: null, + chartDrag: { + dragging: false, + posX: null, + posY: null, + alpha: null, + beta: null, + sensitivity: 5, + }, }; } + componentDidUpdate() { + // Create the chart when the component updates with turk data + const hit = this.props.hit; + if (!hit.loading && + (hit.hitData.length > 0) && + hit.hitData[0].dimensions && + !this.state.chart) { + const curHit = this.props.hit.hitData[0]; + + // Calculate the minLat to get a measurement + let minLat = null; + let minLong = null; + let maxFoot = null; + + const data = curHit.dimensions.points.reduce((acc, val) => { + // Convert latitude and longitude to feet + const DEG_TO_FT = 364488.888889; + const latitudeFeet = (val.latitude * DEG_TO_FT); + const longitudeFeet = (val.longitude * DEG_TO_FT); + if ((minLat === null) || (latitudeFeet < minLat)) { + minLat = latitudeFeet; + } + if ((minLong === null) || (longitudeFeet < minLong)) { + minLong = longitudeFeet; + } + if ((maxFoot === null) || (val.corresponding_height > maxFoot)) { + maxFoot = val.corresponding_height; + } + + const point = [latitudeFeet, val.corresponding_height, longitudeFeet]; + + if (featuresDict[val.feature] === 'Building Point') { + acc.buildingPoints.push(point); + } else if (featuresDict[val.feature] === 'Roof Point') { + acc.roofPoints.push(point); + } + return acc; + }, { buildingPoints: [], roofPoints: [] }); + + // Now clean the data we've create and add new series to support + // the visualization + + const buildingPointColor = '#5882FA'; + const roofPointColor = '#2E2E2E'; + const basePoints = []; + // Points that will visually connect the building points + // to the base points + const buildingBaseConnectPoints = []; + const buildingPoints = data.buildingPoints.map((val) => { + const newLat = val[0] - minLat; + const newLong = val[2] - minLong; + + if (newLat > maxFoot) { + maxFoot = newLat; + } + if (newLong > maxFoot) { + maxFoot = newLong; + } + const newBuildingPoint = [newLat, val[1], newLong]; + // A point with height of 0 + const basePoint = [newLat, 0, newLong]; + // Now connect the base points and the building point + buildingBaseConnectPoints.push({ + color: buildingPointColor, + data: [newBuildingPoint, basePoint], + stickyTracking: false, + enableMouseTracking: false, + }); + + basePoints.push([newLat, 0, newLong]); + return [newLat, val[1], newLong]; + }); + // Add the first point so it connects as a closed shape + if (buildingPoints.length > 0) { + buildingPoints.push(buildingPoints[0]); + basePoints.push(basePoints[0]); + } + + const roofPoints = data.roofPoints.map((val) => { + const newLat = val[0] - minLat; + const newLong = val[2] - minLong; + if (newLat > maxFoot) { + maxFoot = newLat; + } + if (newLong > maxFoot) { + maxFoot = newLong; + } + return [newLat, val[1], newLong]; + }); + // Add the first point so it connects as a closed shape + if (roofPoints.length > 0) { + roofPoints.push(roofPoints[0]); + } + + Highcharts.getOptions().colors = Highcharts.getOptions().colors.map(color => ( + { + radialGradient: { + cx: 0.4, + cy: 0.3, + r: 0.5, + }, + stops: [ + [0, color], + [1, Highcharts.Color(color).brighten(-0.2).get('rgb')], + ], + } + )); + const chart = new Highcharts.Chart({ + chart: { + renderTo: 'chart-container', + // height: '100%', + margin: 100, + marginLeft: 300, + marginRight: 400, + marginTop: 100, + zoomType: 'xyz', + type: 'scatter', + options3d: { + enabled: true, + alpha: 30, + beta: 30, + depth: 250, + viewDistance: 5, + fitToPlot: false, + frame: { + bottom: { size: 1, color: 'rgba(0,0,0,0.02)' }, + back: { size: 1, color: 'rgba(0,0,0,0.04)' }, + side: { size: 1, color: 'rgba(0,0,0,0.06)' }, + }, + }, + }, + title: { + text: 'Roof and Building Points', + }, + subtitle: { + text: 'Click and drag the plot area to rotate in space', + }, + plotOptions: { + scatter: { + marker: { + symbol: 'circle', + }, + lineWidth: 2, + width: 10, + height: 10, + depth: 10, + }, + }, + yAxis: { + title: 'height', + min: 0, + max: maxFoot, + }, + xAxis: { + gridLineWidth: 1, + min: 0, + max: maxFoot, + }, + zAxis: { + showFirstLabel: false, + min: 0, + max: maxFoot, + }, + legend: { + enabled: false, + }, + series: [ + { + name: 'Building Points', + color: buildingPointColor, + data: buildingPoints, + stickyTracking: false, + }, + { + name: 'Base Points', + color: buildingPointColor, + data: basePoints, + showInLegend: false, + enableMouseTracking: false, + }, + { + name: 'Roof Points', + color: roofPointColor, + data: roofPoints, + stickyTracking: false, + lineWidth: 0, + }, + ...buildingBaseConnectPoints, + ], + }); + // We need to set state AFTER the component updates because + // the chart requires that the
it is targetting + // is already rendered + /* eslint-disable react/no-did-update-set-state */ + this.setState({ chart }); + + chart.container.onmousedown = (event) => { + this.setState({ + chartDrag: { + ...this.state.chartDrag, + dragging: true, + posX: event.pageX, + posY: event.pageY, + alpha: this.state.chart.options.chart.options3d.alpha, + beta: this.state.chart.options.chart.options3d.beta, + }, + }); + }; + chart.container.onmousemove = (event) => { + const { dragging, beta, alpha, posX, posY, sensitivity } = this.state.chartDrag; + if (dragging) { + const newBeta = beta + ((posX - event.pageX) / sensitivity); + this.state.chart.options.chart.options3d.beta = newBeta; + + const newAlpha = alpha + ((event.pageY - posY) / sensitivity); + this.state.chart.options.chart.options3d.alpha = newAlpha; + + this.state.chart.redraw(false); + } + }; + chart.container.onmouseup = () => { + this.setState({ + chartDrag: { + ...this.state.chartDrag, + dragging: false, + }, + }); + }; + } + // Resize the chart + if (this.state.chart) { + this.state.chart.reflow(); + } + } + handleHitDecision = (hit, approve) => { const { hitDecision } = this.props; let responseMessage = null; @@ -29,6 +278,10 @@ class TurkHit extends Component { hitDecision(hit, approve, responseMessage); } + toggleDisplayChart = () => { + this.setState({ displayChart: !this.state.displayChart }); + } + renderDefinitions = () => (

Mechanical Turk

@@ -66,7 +319,7 @@ class TurkHit extends Component { ); } - return (); + return (); } renderCreateHitButton = () => { @@ -127,6 +380,36 @@ class TurkHit extends Component { }) ) + renderChartToggleButton = () => ( +
+ +
+ ) + + renderChart = () => { + let chartDiv = ( +
+
+
+
+ ); + if (this.state.displayChart) { + chartDiv = ( +
+
+
+
+ ); + } + + return chartDiv; + } + render() { const { hit } = this.props; const status = hit.status; @@ -136,7 +419,7 @@ class TurkHit extends Component { return (
{this.renderDefinitions()} -

Loading...

+

Loading... Please be patient and do not refresh the page...

); } else if (hit.error && hit.error.response.status !== 404) { @@ -180,6 +463,8 @@ class TurkHit extends Component {
{currStatus.downloadLink && this.renderDownloadLink(curHit.csv_document_key)} {currStatus.fileActions && this.renderHitActions(curHit)} + {this.state.chart && this.renderChartToggleButton()} + {this.renderChart()}
diff --git a/src/components/Utilities/index.js b/src/components/Utilities/index.js index 2fe2a8c7279aefa6d5d932acc0cfc5b58897d8aa..af3dd011d8ac37d1ecb3c1b43430cd114905362b 100644 --- a/src/components/Utilities/index.js +++ b/src/components/Utilities/index.js @@ -146,7 +146,6 @@ class Utilities extends Component { const data = res.data; const disaggregateData = this.parseDisaggregateData(data.disaggregate_csv_output); setDisaggregateData(disaggregateData); - this.resetErrorMessage(); } updateLoadingUploadState(false); diff --git a/src/components/UtilityAccount/index.js b/src/components/UtilityAccount/index.js index e8003ccadb3039ab44eacb45df490d3ae1f159a7..8a521131b0ccfa78afe62d1b44c4636d6d0bf4e3 100644 --- a/src/components/UtilityAccount/index.js +++ b/src/components/UtilityAccount/index.js @@ -157,7 +157,7 @@ class UtilityAccount extends Component { let img = (
-
Upload
Disaggregation + Upload Disaggregation
); if (this.state.convertingFile || this.state.loadingUpload) { @@ -172,7 +172,7 @@ class UtilityAccount extends Component { ); } return ( -
+
{ - const { scrape, disaggregate } = this.state.documentURLs; + renderFetchBtn = () => ( + + ) + + renderDeleteAccountBtn = () => ( + + ) + + renderDisaggregateButtons = () => { + const scrapeKey = this.state.documentKeys.scrape; + const disaggregateKey = this.state.documentKeys.disaggregate; + const scrapeUrl = this.state.documentURLs.scrape; + const disaggregateUrl = this.state.documentURLs.disaggregate; + let scrapeVisibility = 'hidden'; - if (scrape !== undefined && scrape !== '') { + if (scrapeUrl !== undefined && scrapeUrl !== '') { scrapeVisibility = 'visible'; } let disaggregateVisibility = 'hidden'; - if (disaggregate !== undefined && disaggregate !== '') { + if (disaggregateUrl !== undefined && disaggregateUrl !== '') { disaggregateVisibility = 'visible'; } - let chartToggle = (); - let chartDownload = (); + let chartToggle = ( - +

+ {chartToggle} + {chartDownload} +

); } @@ -294,65 +324,90 @@ class UtilityAccount extends Component { } return ( -
- - - -
- ); - } - - render() { - return ( -
-
- - - - - + /> +

+

+

+

+ +

+

+

+ ); + } - {!this.state.disabled ? this.renderAddAccountBtn() : this.renderFetchAndDownloadBtn()} - {this.renderNatGrid()} - {this.showAccountWarning()} - -
+ render() { + return ( +
+
+
+
Account Credentials
+
+
+
+

+ +

+

+ +

+ {this.renderNatGrid()} +
+

+ {this.state.disabled && this.renderFetchBtn()} + {!this.state.disabled ? + this.renderAddAccountBtn() : this.renderDeleteAccountBtn()} +

+ {this.showAccountWarning()} +
+
+
+
+
+
{this.state.disabled ? 'Account Details' : ''}
+
+ {this.state.disabled && this.renderDisaggregateButtons()} +
+
+
{this.renderDisaggregateChart()} +
); } diff --git a/src/containers/Dimensions/propTypes.js b/src/containers/Dimensions/propTypes.js index d0d0b824d582dcf7c60a76eab958295bcd2cc363..c942a4d99e9fd125bdfc528f7085e0fee591ce21 100644 --- a/src/containers/Dimensions/propTypes.js +++ b/src/containers/Dimensions/propTypes.js @@ -2,12 +2,50 @@ import { PropTypes } from 'react'; const { shape, arrayOf, string, number, bool, oneOfType, instanceOf } = PropTypes; +export const buildingDimensionsProps = shape({ + area: number, + perimeter: number, + ground_elevation: number, + number_floors: number, + hit_id: number, + building_id: number, + id: number, +}); + +export const pointsProps = shape({ + latitude: number, + longitude: number, + elevation: number, + adjacent: bool, + feature: number, + hit_id: number, + building_id: number, + id: number, +}); + +export const windowsDoorsProps = shape({ + height: number, + width: number, + quantity: number, + orientation: number, + direction: number, + feature: number, + hit_id: number, + building_id: number, + id: number, +}); + export const hitDataProps = shape({ status_id: number, amazon_hit_id: string, building_id: number, csv_document_key: string, db_id: number, + dimensions: shape({ + building_dimensions: buildingDimensionsProps, + points: arrayOf(pointsProps), + windows_doors: arrayOf(windowsDoorsProps), + }), hit_date: string, requester_name: string, shapefile_document_key: string, diff --git a/src/containers/Documents/sagas.js b/src/containers/Documents/sagas.js index 67b2bc29c25458a81d1499a85081877a9752a8a0..b50202630ecd62e446e7bfb86bb2238f80012201 100644 --- a/src/containers/Documents/sagas.js +++ b/src/containers/Documents/sagas.js @@ -21,24 +21,35 @@ import { function* getDocuments(action) { const { documentPaths, documentKeys, fileKey } = action; - const pathsString = documentPaths.reduce((acc, val) => ( - `${acc}paths[]=${val}&` - ), ''); - const keysString = documentKeys.reduce((acc, val) => ( - `${acc}keys[]=${val}&` - ), ''); - const res = yield call( - request, - `${documentURL}?${pathsString}${keysString}`, { - method: 'GET', - headers: getHeaders(), + const pathsString = documentPaths.reduce((acc, val) => { + if (val) { + return `${acc}paths[]=${val}&`; + } + return acc; + }, ''); + const keysString = documentKeys.reduce((acc, val) => { + if (val) { + return `${acc}keys[]=${val}&`; + } + return acc; + }, ''); + if (keysString || pathsString) { + const res = yield call( + request, + `${documentURL}?${pathsString}${keysString}`, { + method: 'GET', + headers: getHeaders(), + } + ); + if (!res.err) { + yield put(documentsLoaded(res, fileKey)); + } else { + yield put(documentsLoadingError(res.err)); } - ); - - if (!res.err) { - yield put(documentsLoaded(res, fileKey)); } else { - yield put(documentsLoadingError(res.err)); + // There were no keys or paths requested + // An empty call to document service returns empty data + yield put(documentsLoaded({ data: [] }, fileKey)); } }