diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d979a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.idea/* diff --git a/src/components/Home.jsx b/src/components/Home.jsx new file mode 100644 index 0000000..ade823e --- /dev/null +++ b/src/components/Home.jsx @@ -0,0 +1,585 @@ +// @flow +import * as React from 'react'; +import { format } from 'd3'; +import { + Container, + Grid, + List, + ListItem, + ListItemIcon, + ListItemText, + Typography, + withStyles +} from '@material-ui/core'; +import DownTrendIcon from '@material-ui/icons/ArrowDropDown'; +import UpTrendIcon from '@material-ui/icons/ArrowDropUp'; +import FlatTrendIcon from '@material-ui/icons/FiberManualRecord'; +import Control from 'gd-core/src/components/ol/Control'; +import { createEmpty as createEmptyExtent, extend as extendExtent } from 'ol/extent'; +import GeoJSON from 'ol/format/GeoJSON'; +import GroupLayer from 'ol/layer/Group'; +import ImageLayer from 'ol/layer/Image'; +import TileLayer from 'ol/layer/Tile'; +import VectorLayer from 'ol/layer/Vector'; +import ImageWMSSource from 'ol/source/ImageWMS'; +import OSM, { ATTRIBUTION as OSM_ATTRIBUTION } from 'ol/source/OSM'; +import VectorSource from 'ol/source/Vector'; +import XYZ from 'ol/source/XYZ'; +import { decode } from 'geobuf'; +import Pbf from 'pbf'; +import { Map, BaseControlPortal } from 'gd-core/src/components/ol'; +import { entries } from 'gd-core/src/utils/array'; +import { SLRSlope } from 'gd-core/src/utils/math'; + +import type { + Feature as FeatureType, + Map as MapType, + MapBrowserEventType +} from 'ol'; +import type { Layer as LayerType } from 'ol/layer'; + +// TODO: Is it possible to make these dynamic? import data struct from config? +import annualYieldData from '../../data/annual_yield.json'; +import overallData from '../../data/overall_data.json'; +import { HEADERS_HEIGHT } from '../Layout/Header'; + +import Sidebar from './Sidebar'; +import { + CONTEXTUAL_LAYERS, + MAP_BOUNDS, + BOUNDARIES, + GEOSERVER_URL, + getLayerExtent, + getOverallFeatureLabels, + getFeatureStyle, + initialState +} from './config'; + +const styles = { + main: { + height: `calc(100% - ${HEADERS_HEIGHT}px)` + }, + mainContainer: { + position: 'absolute', + height: '100%' + }, + sidebar: { + 'height': '100%', + 'overflowY': 'auto', + '& a': { + color: '#0D73C5' + } + }, + fillContainer: { + width: '100%', + height: '100%' + }, + trendIcon: { + 'fontSize': 18, + '&.red': { + color: '#ff0000' + }, + '&.blue': { + color: '#1e90ff' + }, + '&.black': { + color: '#000' + } + }, + boundaryInfoControl: { + top: '0.5em', + left: '3em', + background: '#fff', + border: '2px solid #aaa', + paddingTop: 10 + }, + legendControl: { + bottom: '0.5em', + left: '0.5em', + background: '#fff', + border: '2px solid #aaa' + }, + legendItem: { + padding: '0 8px' + }, + legendItemIcon: { + minWidth: 25 + } +}; + +type Props = { + classes: { + main: string; + mainContainer: string; + sidebar: string; + fillContainer: string; + trendIcon: string; + boundaryInfoControl: string; + legendControl: string; + legendItem: string; + legendItemIcon: string; + } +} + +type State = { + boundary: string; + featureId: string | null; + regionLabel: string | null; + selectedFeature: FeatureType | null; + year: number; + nutrient: string; +} + +class Home extends React.Component { + map: MapType; + + boundaryInfoControl: Control; + + legendControl: Control; + + layers: { + [key: string]: LayerType + }; + + legends: Array<{ + layerId: string; + title: string; + url: string; + boundaries?: Array; + visible: boolean; + }>; + + constructor(props) { + super(props); + + const [regionLabel, featureId] = getOverallFeatureLabels('drainage'); + this.state = { + featureId, + regionLabel, + selectedFeature: null, + ...initialState + }; + + this.boundaryInfoControl = new Control({ + className: this.props.classes.boundaryInfoControl + }); + + this.legendControl = new Control({ + className: this.props.classes.legendControl + }); + + const geoJSONFormat = new GeoJSON({ + dataProjection: 'EPSG:4326', + featureProjection: 'EPSG:3857' + }); + + this.legends = []; + + this.layers = { + basemaps: new GroupLayer({ + title: 'Base Maps', + layers: [ + new TileLayer({ + type: 'base', + visible: true, + title: 'Carto', + source: new XYZ({ + url: 'https://{a-d}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png', + attributions: [ + '© Carto,', + OSM_ATTRIBUTION + ] + }) + }), + new TileLayer({ + type: 'base', + visible: false, + title: 'OSM', + source: new OSM() + }) + ] + }), + contextual: new GroupLayer({ + title: 'Layers', + layers: CONTEXTUAL_LAYERS.map(({ title, id, boundaries, zIndex }) => { + const source = new ImageWMSSource({ + url: `${GEOSERVER_URL}/wms`, + params: { LAYERS: id }, + ratio: 1, + serverType: 'geoserver' + }); + const visible = !boundaries || boundaries.indexOf(initialState.boundary) > -1; + const layer = new ImageLayer({ + title, + source, + visible, + zIndex + }); + this.legends.push({ + layerId: layer.ol_uid, + title, + url: source.getLegendUrl(), + boundaries, + visible + }); + return layer; + }) + }), + ...entries(BOUNDARIES).reduce( + (boundaryLayers, [name, { visible, layers }]) => { + const group = new GroupLayer({ + layers: layers.map(({ url, style, interactive = false, zIndex = undefined }) => { + const source = new VectorSource({ + loader: (extent) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.responseType = 'arraybuffer'; + const onError = () => { + source.removeLoadedExtent(extent); + }; + xhr.onerror = onError; + xhr.onload = () => { + if (xhr.status === 200) { + const geojson = decode(new Pbf(xhr.response)); + source.addFeatures(geoJSONFormat.readFeatures(geojson)); + } else { + onError(); + } + }; + xhr.send(); + }, + useSpatialIndex: true, + format: geoJSONFormat + }); + const layer = new VectorLayer({ + source, + name, + style: (feature, resolution) => { + const { nutrient, year } = this.state; + return style(feature, resolution, nutrient, year); + } + }); + layer.set('interactive', interactive); + layer.setZIndex(zIndex); + source.on( + 'change', + () => { + if (!group.isReady && source.getState() === 'ready') { + group.isReady = true; + group.setVisible(visible); + } + } + ); + return layer; + }) + }); + boundaryLayers[name] = group; + return boundaryLayers; + }, + {} + ) + }; + } + + updateMap = (map) => { + this.map = map; + + const extent = createEmptyExtent(); + extendExtent(extent, getLayerExtent(initialState.boundary)); + this.map.getView().fit(extent, { duration: 300 }); + + // change cursor when mouse is over interactive layers + this.map.on('pointermove', (e) => { + const pixel = map.getEventPixel(e.originalEvent); + const feature = map.forEachFeatureAtPixel(pixel, (_, layer) => { + return layer.get('interactive'); + }); + map.getTarget().style.cursor = feature ? 'pointer' : ''; + }); + }; + + handleBoundaryChange = (boundary) => { + const { selectedFeature } = this.state; + if (selectedFeature) { + const { nutrient, year } = this.state; + selectedFeature.setStyle( + getFeatureStyle( + selectedFeature, + null, + nutrient, + year, + false + ) + ); + } + this.layers[this.state.boundary].setVisible(false); + + this.layers[boundary].setVisible(true); + const extent = createEmptyExtent(); + extendExtent(extent, getLayerExtent(boundary)); + this.map.getView().fit(extent, { duration: 300 }); + + this.legends.forEach((legend) => { + const { layerId, boundaries } = legend; + const layer = this.layers.contextual.getLayersArray().find(({ ol_uid }) => ol_uid === layerId); + const visible = !boundaries || boundaries.indexOf(boundary) > -1; + layer.setVisible(visible); + legend.visible = visible; + }); + + const [regionLabel, featureId] = getOverallFeatureLabels(boundary); + this.setState({ boundary, featureId, regionLabel, selectedFeature: null }); + }; + + handleVariableChange = (value, variable) => { + this.setState( + { [variable]: value }, + () => { + this.layers[this.state.boundary].getLayers().forEach((layer) => layer.changed()); + const { selectedFeature } = this.state; + if (selectedFeature) { + const { nutrient, year } = this.state; + selectedFeature.setStyle( + getFeatureStyle( + selectedFeature, + null, + nutrient, + year, + true + ) + ); + } + } + ); + }; + + handleMapClick = (event: MapBrowserEventType) => { + const { + featureId: previousFeatureId, + selectedFeature: previousFeature + } = this.state; + + const clickedStationId = event.map.forEachFeatureAtPixel( + event.pixel, + (feature, layer) => { + if (layer.get('interactive')) { + return feature.get('Station_ID'); + } + return false; + } + ); + const selectedFeature = event.map.forEachFeatureAtPixel( + event.pixel, + (feature) => { + if (feature.get('Station_ID') === clickedStationId && feature.getGeometry().getType().indexOf('Polygon') > -1) { + return feature; + } + return false; + }, + { + hitTolerance: 10 + } + ); + + if (selectedFeature) { + const { boundary, nutrient, year } = this.state; + const [regionLabel, overallFeatureId] = getOverallFeatureLabels(boundary); + if (previousFeatureId !== overallFeatureId && previousFeature) { + previousFeature.setStyle( + getFeatureStyle( + previousFeature, + null, + nutrient, + year, + false + ) + ); + } + + const featureId = selectedFeature.get('Name') || selectedFeature.get('Station_ID'); + if (featureId !== previousFeatureId) { + // Feature is selected + selectedFeature.setStyle(getFeatureStyle( + selectedFeature, + null, + nutrient, + year, + true + )); + this.setState({ featureId, selectedFeature }); + } else { + // Feature is deselected + this.setState({ featureId: overallFeatureId, regionLabel, selectedFeature: null }); + } + } + }; + + getNutrientTrend = (nutrient: string, featureName: string): number => { + const x = []; + const y = []; + Object.entries(annualYieldData[nutrient][featureName]).forEach(([year, value]) => { + x.push(parseInt(year, 10)); + y.push(parseFloat(value)); + }); + return SLRSlope(x, y) || 0; + }; + + getTrends = (featureName: string) => { + const classes = this.props.classes; + const nitrogenTrend = this.getNutrientTrend('Nitrogen', featureName); + const phosphorusTrend = this.getNutrientTrend('Phosphorus', featureName); + return [['Nitrogen', nitrogenTrend], ['Phosphorus', phosphorusTrend]].map(([nutrient, trend]) => { + let Icon; + let color; + if (trend > 0) { + Icon = UpTrendIcon; + color = 'red'; + } else if (trend < 0) { + Icon = DownTrendIcon; + color = 'blue'; + } else { + Icon = FlatTrendIcon; + color = 'black'; + } + return ( + <> +
+ + {nutrient} Trend + + + + ); + }); + }; + + getBoundaryInfoContent = () => { + const { selectedFeature, boundary } = this.state; + let featureName; + let contributingWaterways; + let cumulativeAcres; + + if (selectedFeature) { + featureName = selectedFeature.get('Name') || selectedFeature.get('Station_ID'); + const featureProps = selectedFeature.getProperties(); + contributingWaterways = featureProps.contributing_waterways; + cumulativeAcres = featureProps.cumulative_acres; + } else { + featureName = getOverallFeatureLabels(boundary).join(' - '); + contributingWaterways = overallData[boundary].contributing_waterways; + cumulativeAcres = overallData[boundary].cumulative_acres; + } + + return ( + <> + + {featureName} + + + {contributingWaterways ? + {format(',')(contributingWaterways)} Contributing Waterways : + null} +
+ {cumulativeAcres ? + {format(',')(cumulativeAcres)} Cumulative Acres : + null} +
+ + ); + }; + + render() { + const { classes } = this.props; + const { + boundary, + regionLabel, + featureId, + nutrient, + year + } = this.state; + + return ( + { + this.legends.forEach((legend) => { + const { title, visible } = legend; + document.querySelectorAll('.layer-switcher li.layer').forEach((el) => { + if (el.innerText === title) { + if (visible) { + el.classList.remove('hidden'); + } else { + el.classList.add('hidden'); + } + } + }); + }); + } + }} + updateMap={this.updateMap} + events={{ + click: this.handleMapClick + }} + > + + + + + + + + {this.getBoundaryInfoContent()} + + + + + {this.legends.map(({ title, url, visible }) => ( + visible ? + + + {title} + + + : + null + ))} + + + + ); + } +} diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx new file mode 100644 index 0000000..b7090c7 --- /dev/null +++ b/src/components/Sidebar.jsx @@ -0,0 +1,613 @@ +// @flow +import * as React from 'react'; +import { event, select } from 'd3'; +import { + Box, + Container, + Dialog, + DialogContent, + DialogTitle, + Divider, + FormControl, + FormLabel, + Grid, + IconButton, + InputBase, + NativeSelect, + Typography, + makeStyles +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import InfoIcon from '@material-ui/icons/Info'; +import { Link } from 'react-router-dom'; +import { BarChart, LegendHorizontalDiscrete, SimpleLegend } from 'gd-core/src/components/d3'; +import Carousel from 'gd-core/src/components/Carousel'; +import { entries } from 'gd-core/src/utils/array'; +import { useElementRect } from 'gd-core/src/utils/hooks'; + +import dataStories from '../DataStories/pages'; +import DataStoriesModal from '../DataStories/Details'; +import annualYieldData from '../../data/annual_yield.json'; +import annualLoadData from '../../data/annual_load.json'; +import overallData from '../../data/overall_data.json'; +import { + stateName, + dataSource, + getNutrientValueCategoryIndex, + FEATURE_STYLE_INFO, + BOUNDARIES, + VARIABLES_INFO, + SIDEBAR_INFO, +} from './config'; + +type Props = { + regionLabel: string | null; + featureId: string | null; + selectedBoundary: string; + selectedNutrient: string; + selectedYear: number; + handleBoundaryChange: Function; + handleVariableChange: Function; +} + +const useStyle = makeStyles((theme) =>({ + dropdownsContainer: { + background: '#e2ebf4' + }, + header: { + margin: '10px auto' + }, + divider: { + borderTop: '1px dashed #000', + backgroundColor: 'unset' + }, + infoIcon: { + color: '#0D73C5', + fontSize: '1rem' + }, + featureProp: { + color: '#E05769' + }, + formControl: { + margin: theme.spacing(1) + }, + formLabel: { + padding: theme.spacing(1), + fontSize: '.88rem' + }, + selectButton: { + 'background': theme.palette.primary.main, + 'borderRadius': 4, + 'color': theme.palette.primary.contrastText, + 'position': 'relative', + 'height': 42, + 'padding': theme.spacing(2), + 'fontSize': '.75rem', + '&:focus': { + borderRadius: 4 + }, + '& option': { + color: 'initial' + } + }, + annualFlowChart: { + marginTop: -75 + }, + annualFlowLegend: { + '& svg': { + fontSize: '.8rem', + padding: 5, + border: '1px solid #aaa' + } + }, + barChart: { + '& .xAxis .tick:nth-child(2n) text': { + visibility: 'hidden' + } + }, + annualYieldTooltip: { + height: 15 + }, + chartTooltip: { + position: 'fixed', + background: '#fff', + border: '1px solid #eee', + borderRadius: 5, + padding: 5, + opacity: 0 + }, + carousel: { + width: '100%', + marginBottom: 20 + }, + carouselButton: { + 'backgroundColor': '#0D73C5', + '&:hover': { + backgroundColor: '#0D73C5' + } + }, + carouselSlideContainer: { + width: '100%' + }, + carouselSlide: { + width: '100%' + } +})); + +const Sidebar = ({ + regionLabel, + featureId, + selectedBoundary, + selectedNutrient, + selectedYear, + handleBoundaryChange, + handleVariableChange +}: Props) => { + const classes = useStyle(); + + const container = React.useRef(); + const containerRect = useElementRect(container); + + const annualStateFlowChartTooltipRef: { current: null | HTMLDivElement } = React.createRef(); + + const annualYieldTooltipRef: { current: null | HTMLDivElement } = React.createRef(); + const annualYieldChartTooltipRef: { current: null | HTMLDivElement } = React.createRef(); + + const annualLoadChartData = annualLoadData[featureId]; + + const yearsOptions = []; + let annualYieldChartData; + let featureValue; + if (annualYieldData[selectedNutrient][featureId]) { + featureValue = annualYieldData[selectedNutrient][featureId][selectedYear]; + annualYieldChartData = Object + .entries(annualYieldData[selectedNutrient][featureId]) + .map( + ([year, value]) => { + // Data is already sorted by year in `src/data/annual_yield.json` + yearsOptions.push(); + return { + x: year, + y: value, + selected: +year === +selectedYear + }; + } + ); + }; + + const [iframeProps, updateIframeProps] = React.useState({}); + + const handleDataStoriesModalClose = () => updateIframeProps({}); + + const [dialogContent, updateDialogContent] = React.useState(null); + + return ( + <> + + + + + Boundary Type +   + updateDialogContent(VARIABLES_INFO.boundary))} + /> + + + { + handleBoundaryChange(value); + }} + input={} + > + {entries(BOUNDARIES).map(([name, { label }]) => ( + + ))} + + + + + + Nutrient +   + updateDialogContent(VARIABLES_INFO.nutrient))} + /> + + + { + handleVariableChange(value, 'nutrient'); + }} + input={} + > + + + + + + + Year {!yearsOptions.length ? '(N/A)' : ''} + + { + handleVariableChange(value, 'year'); + }} + input={} + > + {yearsOptions} + + + + + + {regionLabel} - {featureId} + + + {selectedBoundary === 'drainage' && featureId === 'Statewide Summary' ? + <> + + + TOTAL {selectedNutrient.toUpperCase()} LEAVING THE STATE OF {stateName.toUpperCase()} + + + The total {selectedNutrient} load leaving the state of {stateName} is estimated to be  + {overallData.drainage.annual_load[selectedNutrient][selectedYear]}  + million lb in {selectedYear}. + + ({ + x: +year, + y: +value, + selected: +year === +selectedYear + }) + ) + } + xAxisProps={{ + title: 'Year', + titlePadding: 55, + stroke: '#4682b4', + strokeWidth: 2 + }} + yAxisProps={{ + title: 'M. lb', + titlePadding: 10, + stroke: '#4682b4', + strokeWidth: 2 + }} + mouseOver={(d) => { + select(annualStateFlowChartTooltipRef.current) + .html(`${d.y} Million lb`) + .transition() + .duration(200) + .style('opacity', .9) + .style('left', `${event.clientX}px`) + .style('top', `${event.clientY - 50}px`); + }} + mouseOut={() => { + select(annualStateFlowChartTooltipRef.current) + .transition() + .duration(500) + .style('opacity', 0); + }} + barStroke={(d) => yearsOptions.length && d.selected ? 'red' : '#117fc9'} + barStrokeWidth={2} + barStrokeOpacity={(d) => d.selected ? 1 : 0} + barFill="#117fc9" + barFillOpacity="1" + lineStroke="#f63700" + lineStrokeWidth={2} + intervalFill="#fdb47f" + width={(containerRect.width || 0) * 0.9} + height={300} + marginTop={50} + marginBottom={60} + marginLeft={60} + marginRight={20} + /> +
+ : + null} + + {selectedBoundary === 'watershed' && annualLoadChartData ? + <> + + + + ANNUAL NITRATE LOAD + + + + ({ + x, + y, + selected: x === +selectedYear + }) + ) + } + lineData={annualLoadChartData.normalized_flow} + intervalData={annualLoadChartData.confidence_interval} + xAxisProps={{ + title: 'Year', + titlePadding: 50, + stroke: '#4682b4', + strokeWidth: 2 + }} + yAxisProps={{ + title: 'Tons', + titlePadding: 10, + stroke: '#4682b4', + strokeWidth: 2 + }} + barStroke={(d) => yearsOptions.length && d.selected ? 'red' : '#117fc9'} + barStrokeWidth={2} + barStrokeOpacity={(d) => d.selected ? 1 : 0} + barFill="#117fc9" + barFillOpacity="1" + lineStroke="#f63700" + lineStrokeWidth={2} + intervalFill="#fdb47f" + width={(window.innerWidth / 3) - 50} + height={300} + marginTop={50} + marginBottom={60} + marginLeft={60} + marginRight={20} + /> + : + null} + {featureValue !== undefined ? + <> + + + AVERAGE YIELD - {selectedYear}: +   + + {featureValue >= 0 ? + `${featureValue} lb/acre` : + 'No data is available'} + + + + FEATURE_STYLE_INFO[getNutrientValueCategoryIndex( + idx === 0 ? undefined : (idx * 5) - 0.1 + )]} + activeBox={getNutrientValueCategoryIndex(featureValue)} + activeBoxLabel={featureValue >= 0 ? featureValue.toString() : ' '} + activeBoxLabelHeight={15} + activeBoxBorderColor="red" + /> + + : + null} + + {annualYieldChartData ? + <> + + + + ANNUAL {selectedNutrient.toUpperCase()} YIELD  + {annualYieldChartData[0].x}-{annualYieldChartData[annualYieldChartData.length - 1].x} +   + updateDialogContent(VARIABLES_INFO.yield))} + /> + + + + d.selected ? 'red' : '#4682b4'} + barStrokeWidth={2} + barStrokeOpacity={(d) => d.selected ? 1 : 0} + barFill={({ y }) => { + const styleInfo = FEATURE_STYLE_INFO[getNutrientValueCategoryIndex(y)]; + return styleInfo.color ? styleInfo.color : '#000'; + }} + barFillOpacity="1" + mouseOver={(d, idx, rects) => { + select(rects[idx]).attr('fill', 'brown'); + select(annualYieldTooltipRef.current) + .html(`${d.x}: ${d.y} lb/acre`); + select(annualYieldChartTooltipRef.current) + .html(`${d.y} lb/acre`) + .transition() + .duration(200) + .style('opacity', .9) + .style('left', `${event.clientX}px`) + .style('top', `${event.clientY - 50}px`); + }} + mouseOut={(d, idx, rects) => { + const styleInfo = FEATURE_STYLE_INFO[getNutrientValueCategoryIndex(d.y)]; + select(rects[idx]) + .attr('fill', styleInfo.color ? styleInfo.color : '#000'); + select(annualYieldTooltipRef.current) + .html(''); + select(annualYieldChartTooltipRef.current) + .transition() + .duration(500) + .style('opacity', 0); + }} + width={(window.innerWidth / 3) - 50} + height={300} + marginTop={50} + marginBottom={60} + marginLeft={60} + marginRight={20} + /> +
+ : + null} + {selectedBoundary === 'drainage' || selectedBoundary === 'huc8' ? + + + {dataSource.label} + + : null} + + + + + + + Learn More About GLTG + + + View All Data Stories + + + + {dataStories.map(({ title, thumbnail, slides }) => ( + updateIframeProps({ + source: slides, + title + })} + > + + + + {title} + + + + ))} + + + {dialogContent ? + updateDialogContent(null)}> + + + {dialogContent.title} + updateDialogContent(null)} + > + + + + + + {dialogContent.description} + + : + null} + + ); +}; + +export default Sidebar; diff --git a/src/components/config.jsx b/src/components/config.jsx new file mode 100644 index 0000000..dc0b7e6 --- /dev/null +++ b/src/components/config.jsx @@ -0,0 +1,346 @@ +// @flow +import * as React from 'react'; +import { DEVICE_PIXEL_RATIO } from 'ol/has'; +import { Fill, Icon, Stroke, Style } from 'ol/style'; +import type FeatureType from 'ol/Feature'; + +// PBF GEOMETRY FILES FOR BOUNDARIES +import huc8 from '../../data/huc8.pbf'; +import watersheds from '../../data/watersheds.pbf'; +import drainage from '../../data/drainage.pbf'; +import monitoringSites from '../../data/monitoring-sites.pbf'; +import watershedMonitoringSites from '../../data/watersheds-monitoring-sites.pbf'; + +// IMAGE FILES FOR MAP FEATURES +import markerMonitoringSite from '../../images/marker_monitoring_site.png'; +import patternNoData from '../../images/pattern_no_data.png'; + +// JSON DATA +import annualYieldData from '../../data/annual_yield.json'; + +export const stateName = 'Illinois'; +export const dataSource = { + url: 'https://www2.illinois.gov/epa/topics/water-quality/watershed-management/excess-nutrients/Documents/NLRS_SCIENCE_ASSESSMENT_UPDATE_2019%20v7_FINAL%20VERSION_web.pdf', + label: 'Illinois Nutrient Reduction Strategy Science Assessment Update 2019' +} + +// Initial selections when page is loaded +export const initialState = { + boundary: 'drainage', + nutrient: 'Nitrogen', + year: 2017 +}; + +export const GEOSERVER_URL = process.env.GEOSERVER_URL || ''; + +// A missing `boundaries` prop from a legend item means it will be shown for all boundary types +export const CONTEXTUAL_LAYERS: Array<{ title: string; id: string; zIndex?: number, boundaries?: Array}> = [ + { title: 'Rivers', id: 'gltg:us-rivers', zIndex: 2 }, + { title: 'State Boundaries', id: 'gltg:us-states' } +]; + +export const getOverallFeatureLabels = (boundary: string) => { + // Returns an array of two items: the first item is the active boundary label, + // and the second item is its variable name in `data.json`, which can be used for rendering labels too. + switch (boundary) { + case 'drainage': + return [null, 'Statewide Summary']; + case 'huc8': + return [null, 'Statewide Summary']; + case 'watershed': + return ['Mississippi River Basin', 'Nutrient Load to Gulf of Mexico']; + default: + return [null, null]; + } +}; + +export const MAP_BOUNDS = [ + -12792231.63426164, + 3246498.818343048, + -8436000.174951272, + 6512287.786512453 +]; + +export const getLayerExtent = (boundary: string) =>{ + // TODO: Can we dynamically determine these? + switch(boundary){ + case 'drainage': + return [-10673131.179092214,4240945.513367433,-9272804.820907786,5703644.486632567]; + case 'huc8': + return [-10673131.179092214,4240945.513367433,-9272804.820907786,5703644.486632567]; + case 'watershed': + return [-10923839.372435283,4545502.562858378,-9523076.314751584,6008657.686866852]; + default: + return MAP_BOUNDS; + } +}; + +export const FEATURE_STYLE_INFO = [ + { + label: 'No data', + image: patternNoData + }, + { + label: '<5', + color: '#EAEDF2' + }, + { + label: '5-9.99', + color: '#C7D6E6' + }, + { + label: '10-14.99', + color: '#93BDD4' + }, + { + label: '15-19.99', + color: '#4D94C1' + }, + { + label: '20-24.99', + color: '#1B64A7' + }, + { + label: '>25 lb/acre', + color: '#062D64' + } +]; + +export const getNutrientValueCategoryIndex = (nutrientLevel?: number): number => { + if ((nutrientLevel !== 0 && !nutrientLevel) || nutrientLevel < 0) { + return 0; + } + if (nutrientLevel < 5) { + return 1; + } + if (nutrientLevel < 10) { + return 2; + } + if (nutrientLevel < 15) { + return 3; + } + if (nutrientLevel < 20) { + return 4; + } + if (nutrientLevel < 25) { + return 5; + } + return 6; +}; + +const noDataPattern = (() => { + const pixelRatio = DEVICE_PIXEL_RATIO; + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.width = 8 * pixelRatio; + canvas.height = 8 * pixelRatio; + // white background + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); + // line + context.strokeStyle = '#b27eb2'; + context.lineWidth = 2; + context.moveTo(0, 0); + context.lineTo(6 * pixelRatio, 3 * pixelRatio); + context.stroke(); + return context.createPattern(canvas, 'repeat'); +})(); + +export const getFeatureStyle = ( + feature: FeatureType, + resolution: ?number, + nutrient: string, + year: number, + isSelected: boolean = false +) => { + const strokeOptions = isSelected ? + { + color: 'red', + width: 4 + } : + { + color: '#7f7f7f', + width: 1 + }; + + const name = feature.get('Name') || feature.get('Station_ID'); + + const nutrientLevel = name ? parseFloat(annualYieldData[nutrient][name][year]) || 0.0 : 0; + + let color; + if (nutrientLevel >= 0) { + const styleInfo = FEATURE_STYLE_INFO[getNutrientValueCategoryIndex(nutrientLevel)]; + color = styleInfo.color ? styleInfo.color : '#000'; + } else { + color = noDataPattern; + } + + return ( + new Style({ + fill: new Fill({ color }), + stroke: new Stroke(strokeOptions), + zIndex: isSelected ? 2 : 1 + }) + ); +}; + +export type BoundaryType = { + [key: string]: { + visible: boolean; + label: string; + layers: Array<{ + url: string; + style: Function; + interactive?: boolean; + zIndex?: number + }>; +}; +} + +export const BOUNDARIES: BoundaryType = { + drainage: { + visible: true, + label: 'IL Drainage', + layers: [ + { + url: drainage, + style: getFeatureStyle + }, + { + url: monitoringSites, + style: () => new Style({ + image: new Icon(({ + src: markerMonitoringSite + })) + }), + zIndex: 3, + interactive: true + } + ] + }, + huc8: { + visible: false, + label: 'IL HUC8', + layers: [ + { + url: huc8, + style: getFeatureStyle, + interactive: true + } + ] + }, + watershed: { + visible: false, + label: 'Trend Watersheds', + layers: [ + { + url: watersheds, + style: getFeatureStyle + }, + { + url: watershedMonitoringSites, + style: () => new Style({ + image: new Icon(({ + src: markerMonitoringSite + })) + }), + zIndex: 3, + interactive: true + } + ] + } +}; + +export const VARIABLES_INFO = { + boundary: { + title: 'Boundary Type', + description: ( +
+ IL Drainage +

+ This view represents the land area that drains through + each of the measurement points represented on the map as + circles with a monitoring buoy. These stations were chosen + as part of the Illinois Nutrient Loss Reduction Strategy + because collectively, they measure nutrients in the runoff + from about 75% of the land area of the state of Illinois, + and can be used to extrapolate the total mass of nutrients, + or nutrient load, leaving the state of Illinois. +

+ HUC 8 +

+ HUCs, or Hydrologic Unit Codes are standardized boundaries + that basically are the boundaries of watersheds and are + often used in water quality tracking. These HUCs are + divided into successively smaller watershed units. HUC-8 is + a medium-sized watershed, and there are 31 such HUCs in the + state of Illinois. The Illinois Nutrient Reduction Strategy + has used modeling to estimate the nutrient yield from all + of the HUC-8s in the State of Illinois. The HUC 8 watershed + boundaries allow for a more localized view of tracking + nutrient loads than some of the larger “Illinois Drainage” + boundaries. +

+ Watershed Boundaries +

+ This view highlights the watershed or the land area that + drains through the point represented on the map as a pin. + These locations are designated in Great Lakes to Gulf as + "Mississippi River Basin Trend Sites" because + calculating water quality trends at these locations can be + used to track progress in reducing nutrient loads from the + watersheds that drain to that point. Many of these + particular sites were selected because they are mostly + contained within a single state, and thus can be used to + track that state’s nutrient reduction progress. +

+ Load to Gulf +

+ This site, the Mississippi River at St. Francisville is + used to measure the total load of nutrients that are + delivered to the Gulf of Mexico in a given water year + (12 Months beginning October 1). This site is used because + it is just upstream from the Gulf, and yet does not behave + like an estuary. Because some Mississippi River water is + diverted to the Atchafalaya River, appropriate corrections + are made to report total load. +

+
+ ) + }, + nutrient: { + title: 'Nutrient Type', + description: ( +
+

+ Nitrogen and Phosphorus are the two main nutrients that cause + the algal blooms that lead to hypoxia in the Gulf of Mexico. +

+

+ Nitrogen – the main source of nitrogen is runoff from + agriculture, though there are other sources as well such + as urban areas and industry. +

+

+ Phosphorus – the main source of phosphorus is wastewater + treatment, though there are other sources as well such + as erosion. +

+
+ ) + }, + yield: { + title: 'Yield', + description: ( +
+ Yield is a measure of nutrients lost per unit area. This measure is useful because + it removes the influence of watershed size in a measurement so that different size + watersheds may be compared. +
+ ) + } +}; + +export const SIDEBAR_INFO = { + +}