Commit 3d02490e authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '192-map-click-accessions-list' into 'master'

Map click accessions list

Closes #192

See merge request genesys-pgr/genesys-ui!192
parents 800310cb 3eb4c959
...@@ -1476,7 +1476,7 @@ ...@@ -1476,7 +1476,7 @@
"NumericListDimension": "NumericListDimension", "NumericListDimension": "NumericListDimension",
"StringListDimension": "StringListDimension" "StringListDimension": "StringListDimension"
}, },
"showRun": "Show run info", "showRun": "Show",
"executionRunsLoaded": "Execution runs loaded successfully" "executionRunsLoaded": "Execution runs loaded successfully"
} }
} }
......
...@@ -9,8 +9,9 @@ import AccessionFilter from 'model/accession/AccessionFilter'; ...@@ -9,8 +9,9 @@ import AccessionFilter from 'model/accession/AccessionFilter';
import AccessionDetails from 'model/accession/AccessionDetails'; import AccessionDetails from 'model/accession/AccessionDetails';
import AccessionMapInfo from 'model/accession/AccessionMapInfo'; import AccessionMapInfo from 'model/accession/AccessionMapInfo';
import AccessionAuditLog from 'model/accession/AccessionAuditLog'; import AccessionAuditLog from 'model/accession/AccessionAuditLog';
import MapLayer from 'model/genesys/MapTileLayer';
import { RECEIVE_ACCESSIONS, RECEIVE_ACCESSION, RECEIVE_ACCESSION_OVERVIEW, APPEND_ACCESSIONS, RECEIVE_ACCESSION_MAPINFO, RECEIVE_ACCESSION_AUDIT_LOG } from 'accessions/constants'; import { RECEIVE_ACCESSIONS, RECEIVE_ACCESSION, RECEIVE_ACCESSION_OVERVIEW, APPEND_ACCESSIONS, RECEIVE_ACCESSION_MAPINFO, RECEIVE_ACCESSION_AUDIT_LOG, RECEIVE_TILE_LAYER } from 'accessions/constants';
import AccessionService from 'service/genesys/AccessionService'; import AccessionService from 'service/genesys/AccessionService';
import { showSnackbar } from 'actions/snackbar'; import { showSnackbar } from 'actions/snackbar';
import Page from 'model/Page'; import Page from 'model/Page';
...@@ -199,3 +200,13 @@ export const overviewAccessions = (filterCode: string) => (dispatch) => { ...@@ -199,3 +200,13 @@ export const overviewAccessions = (filterCode: string) => (dispatch) => {
dispatch(receiveAccessionOverview(null, error)); dispatch(receiveAccessionOverview(null, error));
}); });
}; };
// TileLayers
const receiveTileLayerInfo = (tileLayer: MapLayer) => ({
type: RECEIVE_TILE_LAYER,
payload: {tileLayer},
});
export const updateTileLayerInfo = (tileLayer: MapLayer) => (dispatch) => {
return dispatch(receiveTileLayerInfo(tileLayer));
};
export const RECEIVE_ACCESSIONS = 'accessions/RECEIVE_ACCESSIONS'; export const RECEIVE_ACCESSIONS = 'accessions/RECEIVE_ACCESSIONS';
export const APPEND_ACCESSIONS = 'accessions/APPEND_ACCESSIONS'; export const APPEND_ACCESSIONS = 'accessions/APPEND_ACCESSIONS';
export const RECEIVE_ACCESSION_OVERVIEW = 'accessions/RECEIVE_ACCESSION_OVERVIEW'; export const RECEIVE_ACCESSION_OVERVIEW = 'accessions/RECEIVE_ACCESSION_OVERVIEW';
export const RECEIVE_TILE_LAYER = 'accessions/RECEIVE_TILE_LAYER';
export const RECEIVE_ACCESSION_AUDIT_LOG = 'accessions/RECEIVE_ACCESSION_AUDIT_LOG'; export const RECEIVE_ACCESSION_AUDIT_LOG = 'accessions/RECEIVE_ACCESSION_AUDIT_LOG';
export const RECEIVE_ACCESSION = 'accessions/RECEIVE_ACCESSION'; export const RECEIVE_ACCESSION = 'accessions/RECEIVE_ACCESSION';
export const RECEIVE_ACCESSION_MAPINFO = 'accessions/RECEIVE_ACCESSION_MAPINFO'; export const RECEIVE_ACCESSION_MAPINFO = 'accessions/RECEIVE_ACCESSION_MAPINFO';
......
import update from 'immutability-helper'; import update from 'immutability-helper';
import { IReducerAction } from 'model/common.model'; import { IReducerAction } from 'model/common.model';
import { RECEIVE_ACCESSIONS, RECEIVE_ACCESSION, RECEIVE_ACCESSION_OVERVIEW, APPEND_ACCESSIONS, RECEIVE_ACCESSION_MAPINFO, RECEIVE_ACCESSION_AUDIT_LOG } from 'accessions/constants'; import {
RECEIVE_ACCESSIONS,
RECEIVE_ACCESSION,
RECEIVE_ACCESSION_OVERVIEW,
APPEND_ACCESSIONS,
RECEIVE_ACCESSION_MAPINFO,
RECEIVE_ACCESSION_AUDIT_LOG,
RECEIVE_TILE_LAYER,
} from 'accessions/constants';
import FilteredPage from 'model/FilteredPage'; import FilteredPage from 'model/FilteredPage';
import Accession from 'model/accession/Accession'; import Accession from 'model/accession/Accession';
import AccessionMapInfo from 'model/accession/AccessionMapInfo'; import AccessionMapInfo from 'model/accession/AccessionMapInfo';
import AccessionOverview from 'model/accession/AccessionOverview'; import AccessionOverview from 'model/accession/AccessionOverview';
import AccessionAuditLog from 'model/accession/AccessionAuditLog'; import AccessionAuditLog from 'model/accession/AccessionAuditLog';
import MapLayer, { AVAILABLE_LAYERS } from 'model/genesys/MapTileLayer';
const INITIAL_STATE: { const INITIAL_STATE: {
accession: Accession; accession: Accession;
...@@ -17,6 +26,7 @@ const INITIAL_STATE: { ...@@ -17,6 +26,7 @@ const INITIAL_STATE: {
pagedError: any; pagedError: any;
overview: AccessionOverview; overview: AccessionOverview;
mapInfo: AccessionMapInfo; mapInfo: AccessionMapInfo;
mapLayers: MapLayer[]
} = { } = {
accession: null, accession: null,
auditLog: null, auditLog: null,
...@@ -25,6 +35,7 @@ const INITIAL_STATE: { ...@@ -25,6 +35,7 @@ const INITIAL_STATE: {
pagedError: null, pagedError: null,
overview: null, overview: null,
mapInfo: null, mapInfo: null,
mapLayers: AVAILABLE_LAYERS,
}; };
function publicAccessions(state = INITIAL_STATE, action: IReducerAction) { function publicAccessions(state = INITIAL_STATE, action: IReducerAction) {
...@@ -109,6 +120,16 @@ function publicAccessions(state = INITIAL_STATE, action: IReducerAction) { ...@@ -109,6 +120,16 @@ function publicAccessions(state = INITIAL_STATE, action: IReducerAction) {
overview: {$set: null}, overview: {$set: null},
}); });
} }
case RECEIVE_TILE_LAYER: {
const {tileLayer} = action.payload;
const indexToUpdate = state.mapLayers.findIndex((layerItem) => layerItem.name === tileLayer.name);
return update(state, {
mapLayers: {
[indexToUpdate]: {$set: tileLayer},
},
});
}
default: default:
return state; return state;
......
...@@ -8,19 +8,20 @@ import { loadAccessionsMapInfo } from 'accessions/actions/public'; ...@@ -8,19 +8,20 @@ import { loadAccessionsMapInfo } from 'accessions/actions/public';
import AccessionFilter from 'model/accession/AccessionFilter'; import AccessionFilter from 'model/accession/AccessionFilter';
import Loading from 'ui/common/Loading'; import Loading from 'ui/common/Loading';
import AccessionMapInfo from 'model/accession/AccessionMapInfo'; import AccessionMapInfo from 'model/accession/AccessionMapInfo';
import PageLayout from 'ui/layout/PageLayout'; import MapLayer from 'model/genesys/MapTileLayer';
import ContentHeader from 'ui/common/heading/ContentHeader';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Tabs, { Tab } from 'ui/common/Tabs'; import Tabs, { Tab } from 'ui/common/Tabs';
import PrettyFilters from 'ui/common/filter/PrettyFilters'; import PrettyFilters from 'ui/common/filter/PrettyFilters';
import ButtonBar from 'ui/common/buttons/ButtonBar'; import ButtonBar from 'ui/common/buttons/ButtonBar';
import ContentLayout from 'ui/layout/ContentLayout';
import MapConfigSection from './c/MapConfigSection';
import AccessionService from 'service/genesys/AccessionService'; import AccessionService from 'service/genesys/AccessionService';
let Map; let Map;
let TileLayer; let TileLayer;
let Popup; let Popup;
let Marker; let Rectangle;
const popupContentLimit = 11; const popupContentLimit = 11;
...@@ -33,6 +34,7 @@ interface IMapPageProps { ...@@ -33,6 +34,7 @@ interface IMapPageProps {
currentTab: string; currentTab: string;
filterCode: string; filterCode: string;
loadAccessionsMapInfo: any; loadAccessionsMapInfo: any;
mapLayers: MapLayer[];
} }
const styles = (theme) => ({ const styles = (theme) => ({
...@@ -54,7 +56,8 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -54,7 +56,8 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
]; ];
public state = { public state = {
popupPosition: [], clickLocation: [],
searchBox: null,
geoData: [], geoData: [],
otherCount: 0, otherCount: 0,
}; };
...@@ -64,8 +67,8 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -64,8 +67,8 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
Map = require('react-leaflet').Map; Map = require('react-leaflet').Map;
TileLayer = require('react-leaflet').TileLayer; TileLayer = require('react-leaflet').TileLayer;
Marker = require('react-leaflet').Marker;
Popup = require('react-leaflet').Popup; Popup = require('react-leaflet').Popup;
Rectangle = require('react-leaflet').Rectangle;
} }
} }
...@@ -103,21 +106,27 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -103,21 +106,27 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
console.log(e); console.log(e);
const width = Math.pow(2, currentZoom - 2);
console.log(`zoom=${currentZoom} width=${width} diff=${ 3 / width}`);
const searchBounds: number[][] = [
[ e.latlng.lat - (3 / width), correctLng - (3 / width) ],
[ e.latlng.lat + (3 / width), correctLng + (3 / width) ],
];
const filterWithGeo = { const filterWithGeo = {
...filter, ...filter,
geo: { geo: {
longitude: { longitude: {
ge: correctLng - (3 / currentZoom), ge: searchBounds[0][1],
le: correctLng + (3 / currentZoom), le: searchBounds[1][1],
}, },
latitude: { latitude: {
ge: e.latlng.lat - (3 / currentZoom), ge: searchBounds[0][0],
le: e.latlng.lat + (3 / currentZoom), le: searchBounds[1][0],
}, },
}, },
}; };
AccessionService.geoJson(filterWithGeo, popupContentLimit) AccessionService.geoJson(filterWithGeo, popupContentLimit)
.then((res) => this.setState({popupPosition: [ e.latlng.lat, e.latlng.lng], geoData: res.geoJson, otherCount: res.otherCount})); .then((res) => this.setState({clickLocation: [ e.latlng.lat, e.latlng.lng], searchBox: searchBounds, geoData: res.geoJson, otherCount: res.otherCount}));
}, 200); }, 200);
} }
...@@ -129,10 +138,10 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -129,10 +138,10 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
} }
public render() { public render() {
const {popupPosition, geoData, otherCount} = this.state; const { searchBox, geoData, otherCount } = this.state;
const position = [30, 0]; const position = [30, 0];
const { mapInfo, currentTab, classes, filterCode, t } = this.props; const { mapInfo, mapLayers, currentTab, classes, filterCode, t } = this.props;
if (! mapInfo) { if (! mapInfo) {
return <Loading />; return <Loading />;
...@@ -142,8 +151,11 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -142,8 +151,11 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
const layerUrl = `{s}/acn/tile/{z}/{x}/{y}?f=${filterCode ? filterCode : ''}`; // `&color=${color}`; const layerUrl = `{s}/acn/tile/{z}/{x}/{y}?f=${filterCode ? filterCode : ''}`; // `&color=${color}`;
return ( return (
<PageLayout withFooter> <ContentLayout
<ContentHeader title={ t('accessions.public.p.browse.title') } subTitle={ t('accessions.public.p.browse.subTitle') } /> right={ <MapConfigSection/> }
rightAlwaysCollapsible
customHeaderHeight
>
<Tabs <Tabs
tab={ currentTab } tab={ currentTab }
actions={ actions={
...@@ -174,18 +186,16 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -174,18 +186,16 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
zoom={ 3 } minZoom={ 2 } maxZoom={ 14 } zoom={ 3 } minZoom={ 2 } maxZoom={ 14 }
bounds={ mapInfo.bounds }> bounds={ mapInfo.bounds }>
<TileLayer <TileLayer
zIndex={ 0 }
opacity={ 0.50 } opacity={ 0.50 }
attribution={ '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' } attribution={ '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }
url={ 'https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png' } url={ 'https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png' }
/> />
{ mapLayers && mapLayers.filter((layer) => layer.enabled).map((layer, index) => <TileLayer zIndex={ index + 1 } key={ layer.name } { ...layer }/>) }
<TileLayer <TileLayer
opacity={ 0.50 } zIndex={ mapLayers.length + 1 }
maxZoom={ 7 }
attribution={ '&copy; worldclim.org' }
url={ 'https://{s}.tile.genesys-pgr.org/worldclim1.4/bio1/{z}/{x}/{y}.png' }
subdomains={ [ 'a', 'b', 'c', 'd' ] }
/>
<TileLayer
updateInterval={ 1000 } updateInterval={ 1000 }
updateWhenZooming={ false } updateWhenZooming={ false }
attribution="&amp;copy Accession localities from <a href=&quot;/&quot;>Genesys PGR</a>" attribution="&amp;copy Accession localities from <a href=&quot;/&quot;>Genesys PGR</a>"
...@@ -193,25 +203,29 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -193,25 +203,29 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
subdomains={ mapInfo.tileServers } subdomains={ mapInfo.tileServers }
/> />
{ geoData && geoData.length > 0 && { geoData && geoData.length > 0 &&
<Marker position={ popupPosition } ref={ (marker) => marker && marker.leafletElement.openPopup() } > <Rectangle
bounds={ searchBox }
ref={ (marker) => marker && marker.leafletElement.openPopup() }
>
<Popup open> <Popup open>
<div> <div>
{ geoData.map((feature) => (<div><Link to={ `/a/${feature.properties.uuid}` }>{ `${feature.properties.accessionNumber} ${feature.properties.instCode}` }</Link></div>)) } { geoData.map((feature) => (<div><Link to={ `/a/${feature.properties.uuid}` }>{ `${feature.properties.accessionNumber} ${feature.properties.instCode}` }</Link></div>)) }
{ otherCount > 0 && <div>{ t('accessions.public.p.map.andMore', {otherMore: otherCount}) }</div> } { otherCount > 0 && <div>{ t('accessions.public.p.map.andMore', {otherMore: otherCount}) }</div> }
</div> </div>
</Popup> </Popup>
</Marker> </Rectangle>
} }
</Map> </Map>
} }
</div> </div>
</PageLayout> </ContentLayout>
); );
} }
} }
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => ({
mapInfo: state.accessions.public.mapInfo || undefined, mapInfo: state.accessions.public.mapInfo || undefined,
mapLayers: state.accessions.public.mapLayers,
filterCode: ownProps.match.params.filterCode || '', filterCode: ownProps.match.params.filterCode || '',
currentTab: ownProps.match.params.tab || 'map', // current tab, or ownProps.location.pathname currentTab: ownProps.match.params.tab || 'map', // current tab, or ownProps.location.pathname
}); });
......
import * as React from 'react';
import { connect } from 'react-redux';
// model
import MapLayer from 'model/genesys/MapTileLayer';
// ui
import withStyles from '@material-ui/core/styles/withStyles';
import ExpandFiltersComponent from 'ui/common/filter/ExpandFiltersComponent';
import { Divider } from '@material-ui/core';
import MapLayerConfig from './MapLayerConfig';
/*tslint:disable*/
const styles = (theme) => ({
root: {
overflow: 'hidden' as 'hidden',
},
layersTitle: {
paddingLeft: '8px',
fontSize: '15px',
fontWeight: 700,
lineHeight: '40px',
color: '#81807f',
textTransform: 'uppercase' as 'uppercase',
},
});
interface IMapConfigSectionProps extends React.ClassAttributes<any> {
classes: any;
mapLayers: MapLayer[];
}
class MapConfigSection extends React.Component<IMapConfigSectionProps> {
public render() {
const {classes, mapLayers} = this.props;
return (
<div className={ classes.root }>
<ExpandFiltersComponent title="Configure map"/>
<span className={ classes.layersTitle }>Layers</span>
<Divider/>
{ mapLayers.map((layer) => <MapLayerConfig layer={ layer }/>) }
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
mapLayers: state.accessions.public.mapLayers,
});
export default connect(mapStateToProps, null)(withStyles(styles)(MapConfigSection));
\ No newline at end of file
import * as React from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import { translate } from 'react-i18next';
import withStyles from '@material-ui/core/styles/withStyles';
// actions
import { updateTileLayerInfo } from 'accessions/actions/public';
// model
import MapLayer from 'model/genesys/MapTileLayer';
// ui
import CollapsibleComponentSearch from 'ui/common/filter/CollapsibleComponentSearch';
import { Slider } from '@material-ui/lab';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import { Divider } from '@material-ui/core';
/*tslint:disable*/
const styles = (theme) => ({
root: {
color: 'initial' as 'initial',
whiteSpace: 'initial' as 'initial',
'& > div > div > span ': {
color: 'initial' as 'initial',
fontWeight: 500,
}
},
rootOpened: {
backgroundColor: '#e9e9e9',
},
content: {
backgroundColor: '#f3f2ee',
margin: '0 -20px -20px -20px',
padding: '0 20px 20px 20px',
},
opacitySlider: {
padding: '8px',
display: 'flex' as 'flex',
alignItems: 'center' as 'center'
},
opacitySliderLabel: {
flexShrink: 0,
fontSize: '12px',
position: 'relative' as 'relative',
left: '-8px',
},
description: {
margin: '0 -20px',
padding: '8px 20px',
whiteSpace: 'initial' as 'initial',
},
statusCheckbox: {
width: '100%',
display: 'flex' as 'flex',
justifyContent: 'space-between' as 'space-between',
flexDirection: 'row-reverse' as 'row-reverse',
marginLeft: '0px',
},
copyright: {
color: 'grey',
fontSize: '12px',
}
});
/*tslint:enable*/
interface IMapLayerConfigProps extends React.ClassAttributes<any> {
title: string;
classes: any;
t: any;
layer: MapLayer;
updateTileLayerInfo: (layerInfo: MapLayer) => void;
}
class MapLayerConfig extends React.Component<IMapLayerConfigProps> {
public state = {
show: false,
};
public componentWillMount() {
const {layer} = this.props;
if (layer && layer.enabled) {
this.setState({show: true});
}
}
public render() {
const {classes, layer, t} = this.props;
const {show} = this.state;
return (
<div className={ `${ classes.root } ${ show && classes.rootOpened }` }>
<CollapsibleComponentSearch
title={ layer.title }
collapsed={ !show }
onToggleCollapsed={ (show) => this.setState({show}) }
>
<Divider style={ {margin: '0 -20px'} }/>
<div className={ classes.content }>
<div className={ classes.description }>
<div>{ t(layer.description) }</div>
</div>
<div className={ classes.opacitySlider }>
<div className={ classes.opacitySliderLabel }>{ `Opacity: ${ Number(layer.opacity).toFixed(2) }` }</div>
<Slider
min={ 0 }
step={ 0.05 }
max={ 1.0 }
onChange={ this.handleSliderChange }
value={ layer.opacity }
color="#88ba42"
/>
</div>
<FormControlLabel
className={ classes.statusCheckbox }
label="Enabled"
control={
<Checkbox
checked={ layer.enabled }
onChange={ this.handleCheckBoxChange }
/>
}
/>
</div>
<span className={ `float-right ${ classes.copyright }` }>{ layer.attribution }</span>
</CollapsibleComponentSearch>
</div>
);
}
private handleSliderChange = (e, value) => {
const {updateTileLayerInfo, layer} = this.props;
updateTileLayerInfo({...layer, opacity: value});
}
private handleCheckBoxChange = (e, value) => {
const {updateTileLayerInfo, layer} = this.props;
updateTileLayerInfo({...layer, enabled: value});
}
}
const mapDispatchToProps = (dispatch) => bindActionCreators({
updateTileLayerInfo,
}, dispatch);
export default translate()(connect(null, mapDispatchToProps)(withStyles(styles)(MapLayerConfig)));
class MapLayer {
public name: string;
public title: string;
public description: string;
public url: string;
public enabled: boolean;
public opacity: number;
public attribution: string;
public maxZoom: number;
public subdomains: string[];