unicef/magicbox-maps-prototype

View on GitHub
src/App.js

Summary

Maintainability
D
1 day
Test Coverage
import React, { Component } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
 
// Third-party React components
import 'react-select/dist/react-select.css';
import 'react-virtualized/styles.css';
import 'react-virtualized-select/styles.css';
import Select from 'react-virtualized-select';
import createFilterOptions from 'react-select-fast-filter-options';
 
// Custom React components
import ControlPanel from './components/control-panel';
import Section from './components/section';
import InputGroup from './components/input-group';
import Legend from './components/legend';
import ConnectivityChart from './components/connectivity-chart';
 
// Helpers
import {calculate_index} from './helpers/helper-index-scores';
import apiConfig from './helpers/api-config';
import countConnectivity from './helpers/count-connectivity';
 
// Main style
import './App.css';
// Map colors
const mapColors = {
// higher color will be shown where indexes are 1
higher: '#0068EA',
// lower color will be shown where indexes are 0
lower: '#DCDCDC'
}
 
mapboxgl.accessToken = apiConfig.accessToken
class App extends Component {
constructor(props: Props) {
super(props);
this.state = {
map: {},
lng: -74.2973,
lat: 4.5709,
zoom: 4.5,
connectivityTotals: null,
options: [],
searchValue: '',
schools: {},
regions: {}
};
}
 
Function `componentDidMount` has 118 lines of code (exceeds 25 allowed). Consider refactoring.
componentDidMount() {
const map = new mapboxgl.Map({
container: this.mapContainer,
style: 'mapbox://styles/mapbox/streets-v9',
center: [this.state.lng, this.state.lat],
zoom: this.state.zoom
});
 
this.setState({map});
 
// Promises
let shapesPromise = fetch(apiConfig.shapes).then((response) => response.json())
let schoolsPromise = fetch(apiConfig.schools).then((response) => response.json())
let mapLoadPromise = new Promise((resolve, reject) => {
map.on('load', (e) => {
resolve(map)
})
 
map.on('error', (e) => {
reject(map)
})
})
 
// Set data for regions when regions and map are available
Similar blocks of code found in 2 locations. Consider refactoring.
Promise.all([shapesPromise, mapLoadPromise]).then(([geojson, map]) => {
map.getSource('regions').setData(geojson)
})
 
// Set data for schools when regions and map are available
Similar blocks of code found in 2 locations. Consider refactoring.
Promise.all([schoolsPromise, mapLoadPromise]).then(([geojson, map]) => {
map.getSource('schools').setData(geojson)
})
 
// Handle shapes data
shapesPromise.then(function(myJson) {
// Calculate indexes
myJson.features = calculate_index(
myJson.features, 'population', 'pop'
)
myJson.features = calculate_index(
myJson.features, 'threats', 'threats_index'
)
myJson.features = calculate_index(
myJson.features, 'violence', 'violence_index'
)
 
return myJson
})
 
// Handle school data
schoolsPromise.then((geojson) => {
this.setState({
connectivityTotals: countConnectivity(geojson.features)
})
})
 
// When data arrives, process them in the background
// to build a list of names for the search component
Promise.all([schoolsPromise, shapesPromise]).then(([schoolsGeojson, shapesGeojson]) => {
return new Promise((resolve, reject) => {
let webWorker = new Worker('ww-process-names.js')
 
webWorker.onmessage = (event) => {
resolve(event.data)
}
 
webWorker.onerror = (err) => {
reject(err)
}
 
// send geojsons to worker
webWorker.postMessage([schoolsGeojson, shapesGeojson])
})
}).then((options) => {
this.setState({
options,
filter: createFilterOptions({options})
})
})
 
map.on('move', () => {
const { lng, lat } = map.getCenter();
 
this.setState({
lng: lng.toFixed(4),
lat: lat.toFixed(4),
zoom: map.getZoom().toFixed(2)
});
});
 
map.on('load', function(e) {
map.addLayer({
id: 'regions',
type: 'fill',
// Add a GeoJSON source containing place coordinates and information.
source: {
type: 'geojson',
data: {
type: "FeatureCollection",
features: []
}
},
layout: {
visibility: 'none'
},
paint: {
'fill-opacity': 0.5
}
});
 
map.addLayer({
id: 'schools',
type: 'circle',
// Add a GeoJSON source containing place coordinates and information.
source: {
type: 'geojson',
data: {
type: "FeatureCollection",
features: []
}
},
paint: {
'circle-radius': {
'base': 1.75,
'stops':[[12, 2], [22, 180]]
},
'circle-color': ['get', 'color']
}
});
 
// Add click event to schools layer
map.on('click', 'schools', (e) => {
let coordinates = e.features[0].geometry.coordinates.slice()
let schoolProperties = e.features[0].properties
 
// output all properties besides color
let html = Object.keys(schoolProperties)
.filter((key) => key !== 'color')
.reduce((acc, key) => {
return acc + `<p><strong>${key}:</strong> ${schoolProperties[key]}</p>`
}, '')
 
new mapboxgl.Popup().setLngLat(coordinates).setHTML(html).addTo(map)
})
 
// Change the cursor to a pointer
map.on('mouseenter', 'schools', (e) => {
map.getCanvas().style.cursor = 'pointer'
})
 
map.on('mouseleave', 'schools', (e) => {
map.getCanvas().style.cursor = ''
})
});
}
 
displayLayerHandler(e) {
// layer name should be stored in element's value property
let layerName = e.target.getAttribute('value')
// will be 'visible' or 'none'
let currentState = e.target.checked ? 'visible' : 'none'
 
// Set layer visibility
this.state.map.setLayoutProperty(layerName, 'visibility', currentState)
}
 
changeRegionPaintPropertyHandler(e) {
// Get all checked inputs for regions
let matches = document.querySelectorAll("input[name=region]:checked");
 
// Change layer visibility if there are matches
this.state.map.setLayoutProperty('regions', 'visibility', matches.length ? 'visible' : 'none')
 
if (!matches.length) {
// no region selected
return
}
 
// build the aggregation query
let atts_to_aggregate = Array.prototype.slice.call(matches).reduce((a,t) => {
a.push(['get', t.value])
return a
}, ['+'])
 
// Set new paint property to color the map
this.state.map.setPaintProperty(
'regions',
'fill-color',
// linear interpolation for colors going from lowerColor to higherColor accordingly to aggregation value
['interpolate',
['linear'],
['/', atts_to_aggregate, atts_to_aggregate.length-1],
0, mapColors.lower,
1, mapColors.higher
]
)
}
 
Function `render` has 58 lines of code (exceeds 25 allowed). Consider refactoring.
render() {
return (
<div className="App">
<div>
<div ref={el => this.mapContainer = el} className="mainMap" />
</div>
<ControlPanel>
<Select name="search" placeholder="School or municipality" multi={true} className="search" value={this.state.searchValue} onChange={(selectedOption) => {
let regionFilter = null
let schoolFilter = null
 
// Set current state
this.setState({searchValue: selectedOption})
 
if (selectedOption.length) {
// Create filter for regions to look into all 'NOMBRE's
regionFilter = ['any'].concat(...selectedOption.map((input) => {
return [
['==', ['get', 'NOMBRE_C'], input.value],
['==', ['get', 'NOMBRE_D'], input.value],
['==', ['get', 'NOMBRE_M'], input.value]
]
}))
 
// Create filter for schools to look into every 'name'
schoolFilter = ['any'].concat(selectedOption.map((input) => {
return ['==', ['get', 'name'], input.value]
}))
}
 
// Set filters
this.state.map.setFilter('regions', regionFilter)
this.state.map.setFilter('schools', schoolFilter)
}} filterOptions={this.state.filter} options={this.state.options} arrowRenderer={() => <i className="fas fa-search" />} />
Similar blocks of code found in 2 locations. Consider refactoring.
<Section title="Region threats">
<InputGroup type="checkbox" name="region" group={[
{ value: 'threats_index',
label: 'Natural Disasters Index' },
{ value: 'violence_index',
label: 'Violence Index' }
]} onChange={this.changeRegionPaintPropertyHandler.bind(this)} />
</Section>
Similar blocks of code found in 2 locations. Consider refactoring.
<Section title="Region vulnerabilities">
<InputGroup type="checkbox" name="region" group={[
{ value: 'hdi',
label: 'Human Development Index' },
{ value: 'pop',
label: 'Population' }
]} onChange={this.changeRegionPaintPropertyHandler.bind(this)} />
</Section>
<Section title="School Capabilities">
<InputGroup type="checkbox" name="school" group={[
{ value: 'schools',
label: 'Connectivity',
onChange: this.displayLayerHandler.bind(this),
defaultChecked: 'checked'
}
]} onChange={(e) => {}} />
</Section>
<p className="controlPanel__footerMessage">The selected items will be considered when calculating the risk level of schools and areas.</p>
<Section title="Connectivity Details">
<ConnectivityChart totals={this.state.connectivityTotals}></ConnectivityChart>
</Section>
</ControlPanel>
<Legend from={mapColors.higher} to={mapColors.lower} steps={10} leftText="Most Risk" rightText="Least Risk" />
</div>
);
}
}
 
export default App;