Skip to content
This repository has been archived by the owner on Mar 30, 2021. It is now read-only.

Map Explorer - Export Data as CSV Functionality #80

Merged
merged 4 commits into from
Mar 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/constants/mapExplorer.config.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"cartodb": {
"vizUrl": "https://shaunanoordin-zooniverse.cartodb.com/api/v2/viz/e04c2e20-a8a9-11e5-8d6b-0e674067d321/viz.json",
"sqlApi": "https://shaunanoordin-zooniverse.cartodb.com/api/v2/sql?q={SQLQUERY}",
"sqlTable": "wildcam_gorongosa_compiled_201601",
"sqlTableCameras": "wildcam_gorongosa_cameras_201601",
"sqlTableSubjects": "wildcam_gorongosa_subjects_201601",
"sqlTableClassifications": "wildcam_gorongosa_classifications_201601",
"sqlQueryCountCameras": "SELECT cameras.*, COUNT(items.*) as count FROM {CAMERAS} AS cameras LEFT JOIN (SELECT {SUBJECTS}.camera, {SUBJECTS}.location, {SUBJECTS}.dateutc, {SUBJECTS}.month, {SUBJECTS}.year, {SUBJECTS}.season, {SUBJECTS}.time_period, {CLASSIFICATIONS}.species, {CLASSIFICATIONS}.species_count, {CLASSIFICATIONS}.user_hash, {SUBJECTS}.subject_id, {CLASSIFICATIONS}.classification_id FROM {SUBJECTS} INNER JOIN {CLASSIFICATIONS} ON {SUBJECTS}.subject_id = {CLASSIFICATIONS}.subject_zooniverse_id) AS items ON cameras.id = items.camera {WHERE} GROUP BY cameras.cartodb_id",
"cssStandard": "#{LAYER} { marker-fill: {MARKER-COLOR}; marker-fill-opacity: {MARKER-OPACITY}; marker-width: {MARKER-SIZE}; marker-line-color: #FFF; marker-line-width: 1; marker-line-opacity: 1; marker-placement: point; marker-type: ellipse; marker-allow-overlap: true; {CHILDREN} }"
"sqlQuerySelectItems": "SELECT cameras.*, items.* FROM {CAMERAS} AS cameras LEFT JOIN (SELECT {SUBJECTS}.camera, {SUBJECTS}.location, {SUBJECTS}.dateutc, {SUBJECTS}.month, {SUBJECTS}.year, {SUBJECTS}.season, {SUBJECTS}.time_period, {CLASSIFICATIONS}.species, {CLASSIFICATIONS}.species_count, {CLASSIFICATIONS}.user_hash, {SUBJECTS}.subject_id, {CLASSIFICATIONS}.classification_id FROM {SUBJECTS} INNER JOIN {CLASSIFICATIONS} ON {SUBJECTS}.subject_id = {CLASSIFICATIONS}.subject_zooniverse_id) AS items ON cameras.id = items.camera {WHERE} LIMIT 1000",
"cssStandard": "#{LAYER} { marker-fill: {MARKERCOLOR}; marker-fill-opacity: {MARKEROPACITY}; marker-width: {MARKERSIZE}; marker-line-color: #FFF; marker-line-width: 1; marker-line-opacity: 1; marker-placement: point; marker-type: ellipse; marker-allow-overlap: true; {CHILDREN} }"
},
"mapCentre": {
"latitude": -18.9178413223141,
Expand Down
22 changes: 13 additions & 9 deletions src/containers/MapExplorer-SelectorData.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ export default class SelectorData {
this.css = this.calculateCss();
}

calculateSql() {
calculateSql(sqlQueryTemplate = config.cartodb.sqlQueryCountCameras) {
return sqlQueryTemplate
.replace(/{CAMERAS}/ig, config.cartodb.sqlTableCameras)
.replace(/{SUBJECTS}/ig, config.cartodb.sqlTableSubjects)
.replace(/{CLASSIFICATIONS}/ig, config.cartodb.sqlTableClassifications)
.replace(/{WHERE}/ig, this.calculateSqlWhereClause());
}

calculateSqlWhereClause() {
//The biggest variable in the SQL query is the 'WHERE' clause.

//Where constructor: species
Expand Down Expand Up @@ -99,11 +107,7 @@ export default class SelectorData {
//user_hash: msyfoopoo99
//location: http://zooniverse-export.s3-website-us-east-1.amazonaws.com/21484_1000_C08_Season%201_Set%201_EK005157.JPG

return config.cartodb.sqlQueryCountCameras
.replace(/{CAMERAS}/ig, config.cartodb.sqlTableCameras)
.replace(/{SUBJECTS}/ig, config.cartodb.sqlTableSubjects)
.replace(/{CLASSIFICATIONS}/ig, config.cartodb.sqlTableClassifications)
.replace(/{WHERE}/ig, sqlWhere);
return sqlWhere;
}

calculateCss() {
Expand All @@ -118,9 +122,9 @@ export default class SelectorData {

return config.cartodb.cssStandard
.replace(/{LAYER}/ig, config.cartodb.sqlTableCameras) //Actually, any ID will do
.replace(/{MARKER-COLOR}/ig, this.markerColor)
.replace(/{MARKER-OPACITY}/ig, this.markerOpacity)
.replace(/{MARKER-SIZE}/ig, this.markerSize)
.replace(/{MARKERCOLOR}/ig, this.markerColor)
.replace(/{MARKEROPACITY}/ig, this.markerOpacity)
.replace(/{MARKERSIZE}/ig, this.markerSize)
.replace(/{CHILDREN}/ig, children);
}

Expand Down
130 changes: 125 additions & 5 deletions src/containers/MapExplorer-SelectorPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import React from 'react';
const config = require('../constants/mapExplorer.config.json');
import SelectorData from './MapExplorer-SelectorData.jsx';
import fetch from 'isomorphic-fetch';

const DIALOG_IDLE = 'idle';
const DIALOG_MESSAGE = 'message';
const DIALOG_DOWNLOAD = 'download-me';

export default class SelectorPanel extends React.Component {
constructor(props) {
Expand All @@ -10,32 +15,45 @@ export default class SelectorPanel extends React.Component {
this.refreshUI = this.refreshUI.bind(this);
this.updateMe = this.updateMe.bind(this);
this.deleteMe = this.deleteMe.bind(this);
this.prepareCsv = this.prepareCsv.bind(this);
this.downloadCsv = this.downloadCsv.bind(this);
this.changeToGuided = this.changeToGuided.bind(this);
this.changeToAdvanced = this.changeToAdvanced.bind(this);
this.changeToAdvanced = this.changeToAdvanced.bind(this);
this.closeDialog = this.closeDialog.bind(this);
this.noAction = this.noAction.bind(this);

//Initialise state
this.state = {
status: DIALOG_IDLE,
message: '',
data: null
};
}

render() {
let thisId = this.props.selectorData.id;

//Input Choice: Species
let species = [];
config.species.map((item) => {
species.push(
<li key={'species_'+item.id}><input type="checkbox" id={'inputRow_species_item_' + item.id} ref={'inputRow_species_item_' + item.id} value={item.id} onchange={this.refreshUI} /><label htmlFor={'inputRow_species_item_' + item.id}>{item.displayName}</label></li>
<li key={'species_'+item.id}><input type="checkbox" id={'inputRow_species_item_' + item.id + '_' + thisId} ref={'inputRow_species_item_' + item.id} value={item.id} onchange={this.refreshUI} /><label htmlFor={'inputRow_species_item_' + item.id + '_' + thisId}>{item.displayName}</label></li>
);
});

//Input Choice: Habitats
let habitats = [];
config.habitats.map((item) => {
habitats.push(
<li key={'habitat_'+item.id}><input type="checkbox" id={'inputRow_habitats_item_' + item.id} ref={'inputRow_habitats_item_' + item.id} value={item.id} onchange={this.refreshUI} /><label htmlFor={'inputRow_habitats_item_' + item.id}>{item.displayName}</label></li>
<li key={'habitat_'+item.id}><input type="checkbox" id={'inputRow_habitats_item_' + item.id + '_' + thisId} ref={'inputRow_habitats_item_' + item.id} value={item.id} onchange={this.refreshUI} /><label htmlFor={'inputRow_habitats_item_' + item.id + '_' + thisId}>{item.displayName}</label></li>
);
});

//Input Choice: Seasons
let seasons = [];
config.seasons.map((item) => {
seasons.push(
<li key={'seasons_'+item.id}><input type="checkbox" id={'inputRow_seasons_item_' + item.id} ref={'inputRow_seasons_item_' + item.id} value={item.id} onchange={this.refreshUI} /><label htmlFor={'inputRow_seasons_item_' + item.id}>{item.displayName}</label></li>
<li key={'seasons_'+item.id}><input type="checkbox" id={'inputRow_seasons_item_' + item.id + '_' + thisId} ref={'inputRow_seasons_item_' + item.id} value={item.id} onchange={this.refreshUI} /><label htmlFor={'inputRow_seasons_item_' + item.id + '_' + thisId}>{item.displayName}</label></li>
);
});

Expand Down Expand Up @@ -97,6 +115,18 @@ export default class SelectorPanel extends React.Component {
<section className="action-subpanel">
<button onClick={this.updateMe}>(Update)</button>
<button onClick={this.deleteMe}>(Delete)</button>
<button onClick={this.prepareCsv}>(Update Map and Prepare CSV)</button>
</section>
<section className={(this.state.status === DIALOG_IDLE) ? 'dialog-screen' : 'dialog-screen enabled' } onClick={this.closeDialog}>
{(this.state.status === DIALOG_MESSAGE) ?
<div className="dialog-box" onClick={this.noAction}>{this.state.message}</div>
: null}
{(this.state.status === DIALOG_DOWNLOAD) ?
<div className="dialog-box" onClick={this.noAction}>
<div>{this.state.message}</div>
<div><a download="WildcamGorongosa.csv" className="btn" onClick={this.downloadCsv}>Download</a></div>
</div>
: null}
</section>
</article>
);
Expand Down Expand Up @@ -154,6 +184,27 @@ export default class SelectorPanel extends React.Component {

//----------------------------------------------------------------

closeDialog(e) {
this.setState({
status: DIALOG_IDLE,
message: '',
data: null
});
}

//'Eats up' events to prevent them from bubbling to a parent element.
noAction(e) {
if (e) {
e.preventDefault && e.preventDefault();
e.stopPropagation && e.stopPropagation();
e.returnValue = false;
e.cancelBubble = true;
}
return false;
}

//----------------------------------------------------------------

//Tells the parent that this Selector has updated its values.
updateMe(e) {
//Create a copy of the current Selector Data, which we will then modify and
Expand Down Expand Up @@ -201,7 +252,7 @@ export default class SelectorPanel extends React.Component {

//Filter control: mode
if (data.mode === SelectorData.GUIDED_MODE) {
this.refs.sql.value = data.calculateSql();
this.refs.sql.value = data.calculateSql(config.cartodb.sqlQueryCountCameras);
this.refs.css.value = data.calculateCss();
}
data.sql = this.refs.sql.value;
Expand All @@ -216,6 +267,75 @@ export default class SelectorPanel extends React.Component {
this.props.deleteMeHandler(this.props.selectorData.id);
}

//Download the current results into a CSV.
prepareCsv(e) {
//First things first: make sure the user sees what she/he is going to download.
this.updateMe(null);

this.setState({
status: DIALOG_MESSAGE,
message: 'Preparing CSV file...',
data: null
});

let sqlQuery = this.props.selectorData.calculateSql(config.cartodb.sqlQuerySelectItems);
console.log('Prepare CSV: ', sqlQuery);
fetch(config.cartodb.sqlApi.replace('{SQLQUERY}', encodeURI(sqlQuery)))
.then((response) => {
if (response.status !== 200) {
throw 'Can\'t reach CartoDB API, HTTP response code ' + response.status;
}
return response.json();
})
.then((json) => {
let data = [];
let row = [];

for (let key in json.fields) {
row.push('"'+key.replace(/"/g, '\\"')+'"');
}
row = row.join(',');
data.push(row);

json.rows.map((rowItem) => {
let row = [];
for (let key in json.fields) {
(json.fields[key].type === 'string' && rowItem[key])
? row.push('"'+rowItem[key].replace(/"/g, '\\"')+'"')
: row.push(rowItem[key]);
}
row = row.join(',');
data.push(row);
});
data = data.join('\n');

this.setState({
status: DIALOG_DOWNLOAD,
message: 'CSV ready!',
data: data
});
})
.catch((err) => {
console.log(err);
this.setState({
status: DIALOG_MESSAGE,
message: 'ERROR',
data: null
});
});
}

downloadCsv(e) {
if (this.state.data) {
let dataBlob = new Blob([this.state.data], {type: 'text/csv'});
let dataAsAFile = window.URL.createObjectURL(dataBlob);
window.open(dataAsAFile);
window.URL.revokeObjectURL(dataAsAFile);
} else {
console.error('Download CSV Error: no CSV');
}
}

//Update the UI based on user actions.
refreshUI(e) {
console.log('Selectors.refreshUI()');
Expand Down
35 changes: 33 additions & 2 deletions src/styles/components/map-explorer.styl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
flex: 0 0 40%
min-height: 20px
overflow: auto
overflow-x: hidden
overflow-y: scroll

.message
margin: 0.5em
Expand Down Expand Up @@ -99,8 +101,10 @@
.range-input
display: flex

input
flex: 1 1 30%
input
display: block
flex: 1 1 auto
width: 20%
margin: 0 0.5em
text-align: center

Expand All @@ -109,3 +113,30 @@

.action-subpanel
text-align: center

.dialog-screen
display: none
flex-direction: column
justify-content: center
align-items: center
position: fixed
left: 0
top: 0
width: 100%
height: 100%
background: rgba(0, 0, 0, 0.5)
z-index: 1000000
cursor: pointer

&.enabled
display: flex

.dialog-box
flex: 0 0 auto
background: #fff
padding: 1em
border-radius: 1em
cursor: default

> div
text-align: center