Commit 65a1d30a authored by Matija Obreza's avatar Matija Obreza

Merge branch '275-accessions-map-list-selected-accessions' into 'master'

Accessions map: list selected accessions

Closes #275

See merge request genesys-pgr/genesys-ui!288
parents e3674e8b f33c57b0
...@@ -442,6 +442,7 @@ ...@@ -442,6 +442,7 @@
"andMore": "And {{otherMore}} more", "andMore": "And {{otherMore}} more",
"kml": "KML", "kml": "KML",
"filterAccessions": "Filter accessions", "filterAccessions": "Filter accessions",
"selectArea": "Select area",
"pick": "Show climate", "pick": "Show climate",
"stopPick": "Cancel", "stopPick": "Cancel",
"noClimateData": "No climate data available for selected location", "noClimateData": "No climate data available for selected location",
......
...@@ -388,9 +388,9 @@ ...@@ -388,9 +388,9 @@
"dev": true "dev": true
}, },
"@types/prop-types": { "@types/prop-types": {
"version": "15.5.8", "version": "15.7.1",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.8.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz",
"integrity": "sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw==" "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg=="
}, },
"@types/q": { "@types/q": {
"version": "1.5.2", "version": "1.5.2",
...@@ -399,9 +399,9 @@ ...@@ -399,9 +399,9 @@
"dev": true "dev": true
}, },
"@types/react": { "@types/react": {
"version": "16.7.18", "version": "16.8.6",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.7.18.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.6.tgz",
"integrity": "sha512-Tx4uu3ppK53/iHk6VpamMP3f3ahfDLEVt3ZQc8TFm30a1H3v9lMsCntBREswZIW/SKrvJjkb3Hq8UwO6GREBng==", "integrity": "sha512-bN9qDjEMltmHrl0PZRI4IF2AbB7V5UlRfG+OOduckVnRQ4VzXVSzy/1eLAh778IEqhTnW0mmgL9yShfinNverA==",
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^2.2.0" "csstype": "^2.2.0"
...@@ -6941,6 +6941,11 @@ ...@@ -6941,6 +6941,11 @@
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
"integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU="
}, },
"is-mobile": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-2.0.0.tgz",
"integrity": "sha512-k2+p7BBCzhqHMdYJwGUNNo+6zegGiMIVbM6bEPzxWXpQV6BUzV892UW0oDFgqxT6DygO7LdxRbwC0xmOhJdbew=="
},
"is-negated-glob": { "is-negated-glob": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
......
...@@ -78,6 +78,7 @@ ...@@ -78,6 +78,7 @@
"andMore": "And {{otherMore}} more", "andMore": "And {{otherMore}} more",
"kml": "KML", "kml": "KML",
"filterAccessions": "Filter accessions", "filterAccessions": "Filter accessions",
"selectArea": "Select area",
"pick": "Show climate", "pick": "Show climate",
"stopPick": "Cancel", "stopPick": "Cancel",
"noClimateData": "No climate data available for selected location", "noClimateData": "No climate data available for selected location",
......
...@@ -3,6 +3,7 @@ import { connect } from 'react-redux'; ...@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { isMobile } from 'is-mobile';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import {showSnackbar} from 'actions/snackbar'; import {showSnackbar} from 'actions/snackbar';
import navigateTo from 'actions/navigation'; import navigateTo from 'actions/navigation';
...@@ -35,6 +36,7 @@ import Paper from '@material-ui/core/Paper'; ...@@ -35,6 +36,7 @@ import Paper from '@material-ui/core/Paper';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import LayersIcon from '@material-ui/icons/Layers'; import LayersIcon from '@material-ui/icons/Layers';
import ClimateIcon from '@material-ui/icons/WbSunny'; import ClimateIcon from '@material-ui/icons/WbSunny';
import PositionIcon from '@material-ui/icons/SettingsOverscan';
import FilterIcon from '@material-ui/icons/PermDataSetting'; import FilterIcon from '@material-ui/icons/PermDataSetting';
import CancelIcon from '@material-ui/icons/Cancel'; import CancelIcon from '@material-ui/icons/Cancel';
import ApiCall from 'model/ApiCall'; import ApiCall from 'model/ApiCall';
...@@ -137,6 +139,10 @@ const styles = (theme) => ({ ...@@ -137,6 +139,10 @@ const styles = (theme) => ({
position: 'absolute' as 'absolute', position: 'absolute' as 'absolute',
top: '42px', top: '42px',
}, },
pickRectangleButton: {
position: 'absolute' as 'absolute',
top: '80px',
},
controlsButton: { controlsButton: {
width: '50px', width: '50px',
height: '50px', height: '50px',
...@@ -180,11 +186,15 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -180,11 +186,15 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
otherCount: 0, otherCount: 0,
sidebarOpened: false, sidebarOpened: false,
trackClickPos: false, trackClickPos: false,
trackAreaSelect: false,
dialogOpened: false, dialogOpened: false,
climateData: null, climateData: null,
layersControlsIsOpen: false, layersControlsIsOpen: false,
savedControlsIsOpen: false, savedControlsIsOpen: false,
colorInputIsFocused: false, colorInputIsFocused: false,
mouseDown: false,
areaStartPos: [0, 0],
areaEndPos: [0, 0],
}; };
constructor(props, context) { constructor(props, context) {
...@@ -247,7 +257,19 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -247,7 +257,19 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
private setPositionPick = (e) => { private setPositionPick = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.setState({trackClickPos: !this.state.trackClickPos}); this.setState({trackClickPos: !this.state.trackClickPos, trackAreaSelect: false});
return false;
}
private setAreaTrackPick = (e) => {
e.preventDefault();
e.stopPropagation();
if (typeof window !== 'undefined' && isMobile()) {
return this.handleMobileAreaSelect();
}
this.setState({trackAreaSelect: !this.state.trackAreaSelect, trackClickPos: false, areaStartPos: [0, 0], areaEndPos: [0, 0]});
return false; return false;
} }
...@@ -272,6 +294,33 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -272,6 +294,33 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
}); });
} }
private handleMobileAreaSelect = () => {
const mapBounds = this.mapRef.leafletElement.getBounds();
const {mapInfo: { data: { filter } }} = this.props;
const filterWithGeo = {
...filter,
geo: {
longitude: {
ge: mapBounds._southWest.lng,
le: mapBounds._northEast.lng,
},
latitude: {
ge: mapBounds._southWest.lat,
le: mapBounds._northEast.lat,
},
climate: filter && filter.geo && filter.geo.climate,
},
};
this.myApplyFilters(filterWithGeo);
}
private handleTrackAreaStart = (e) => {
const { lat, lng } = e.latlng;
this.setState({areaStartPos: [lat, lng], areaEndPos: [lat, lng]});
}
private hideDialog = () => { private hideDialog = () => {
this.setState({dialogOpened: false}); this.setState({dialogOpened: false});
} }
...@@ -287,16 +336,75 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -287,16 +336,75 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
} }
} }
private handleMove = (e) => {
if (this.state.trackAreaSelect && this.mapRef) {
this.setState({areaEndPos: [e.latlng.lat, e.latlng.lng]});
}
}
private handleMouseUp = (e) => {
const {areaStartPos, areaEndPos, trackAreaSelect} = this.state;
if (trackAreaSelect && (areaStartPos[0] || areaStartPos[0] || areaEndPos[1] || areaEndPos[1])) {
const currentZoom = e.target._zoom;
if (areaStartPos[0] - areaEndPos[0] < 3 / Math.pow(2, currentZoom - 2) && areaStartPos[1] - areaEndPos[1] < 3 / Math.pow(2, currentZoom - 2)) {
this.setState({trackAreaSelect: false, mouseDown: false});
return;
}
const {mapInfo: { data: { filter } }} = this.props;
const filterWithGeo = {
...filter,
geo: {
latitude: {
ge: Math.min(areaStartPos[0], areaEndPos[0]),
le: Math.max(areaStartPos[0], areaEndPos[0]),
},
longitude: {
ge: Math.min(areaStartPos[1], areaEndPos[1]),
le: Math.max(areaStartPos[1], areaEndPos[1]),
},
climate: filter && filter.geo && filter.geo.climate,
},
};
this.myApplyFilters(filterWithGeo);
}
this.setState({trackAreaSelect: false, mouseDown: false});
}
private inFilterBounds = (searchBounds) => {
const {mapInfo: { data: { filter } }} = this.props;
// no filter = always in bounds
if (!filter || !filter.geo) {
return true;
}
// longitude check
if (searchBounds[0][1] < filter.geo.longitude.ge || searchBounds[1][1] > filter.geo.longitude.le) {
return false;
}
// latitude check
return !(searchBounds[0][0] < filter.geo.latitude.ge || searchBounds[1][0] > filter.geo.latitude.le);
}
private onMapClick = (e) => { private onMapClick = (e) => {
if (e.originalEvent.target.className.indexOf('leaflet-touch') === -1) { if (e.originalEvent.target.className.indexOf('leaflet-touch') === -1) {
return; return;
} }
this.setState({mouseDown: true});
if (this.state.trackClickPos) { if (this.state.trackClickPos) {
return this.handleTrackPosition(e); return this.handleTrackPosition(e);
} }
if (this.state.trackAreaSelect) {
return this.handleTrackAreaStart(e);
}
if (this.clickTimeout) { if (this.clickTimeout) {
console.log('prevented'); console.log('prevented');
clearTimeout(this.clickTimeout); clearTimeout(this.clickTimeout);
...@@ -315,14 +423,17 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -315,14 +423,17 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
const correctLng = Math.abs(e.latlng.lng) > 180 ? (360 + e.latlng.lng) % 180 : e.latlng.lng; const correctLng = Math.abs(e.latlng.lng) > 180 ? (360 + e.latlng.lng) % 180 : e.latlng.lng;
console.log(e);
const width = Math.pow(2, currentZoom - 2); const width = Math.pow(2, currentZoom - 2);
// console.log(`zoom=${currentZoom} width=${width} diff=${ 3 / width}`); // console.log(`zoom=${currentZoom} width=${width} diff=${ 3 / width}`);
const searchBounds: number[][] = [ const searchBounds: number[][] = [
[ e.latlng.lat - (3 / width), correctLng - (3 / width) ], [ e.latlng.lat - (3 / width), correctLng - (3 / width) ],
[ e.latlng.lat + (3 / width), correctLng + (3 / width) ], [ e.latlng.lat + (3 / width), correctLng + (3 / width) ],
]; ];
if (!this.inFilterBounds(searchBounds)) {
return;
}
const filterWithGeo = { const filterWithGeo = {
...filter, ...filter,
geo: { geo: {
...@@ -350,7 +461,7 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -350,7 +461,7 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
} }
public render() { public render() {
const { searchBox, geoData, otherCount, sidebarOpened, trackClickPos, dialogOpened, climateData, layersControlsIsOpen, savedControlsIsOpen } = this.state; const { searchBox, geoData, otherCount, sidebarOpened, trackClickPos, dialogOpened, climateData, layersControlsIsOpen, savedControlsIsOpen, trackAreaSelect, mouseDown, areaStartPos, areaEndPos } = this.state;
const { mapInfo, mapLayers, currentTab, classes, filterCode, loading, suggestions, t, loadAccessionsMapInfo, initialPosition, initialZoom, myMaps, addToMyMaps, toggleMyMap } = this.props; const { mapInfo, mapLayers, currentTab, classes, filterCode, loading, suggestions, t, loadAccessionsMapInfo, initialPosition, initialZoom, myMaps, addToMyMaps, toggleMyMap } = this.props;
const position = initialPosition[0] && initialPosition[1] ? initialPosition : [5, 5]; const position = initialPosition[0] && initialPosition[1] ? initialPosition : [5, 5];
...@@ -433,13 +544,16 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -433,13 +544,16 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
displayName="accessions.common.modelName" displayName="accessions.common.modelName"
amount={ mapInfo.data.accessionCount } amount={ mapInfo.data.accessionCount }
/> />
<div className={ `${classes.leafletContainer} ${trackClickPos && classes.crosshair}` }> <div className={ `${classes.leafletContainer} ${(trackClickPos || trackAreaSelect) && classes.crosshair}` }>
{ loading && <Loading /> } { loading && <Loading /> }
{ mapInfo && typeof window !== 'undefined' && { mapInfo && typeof window !== 'undefined' &&
<MapComponent <MapComponent
onMoveend={ this.handleMoveEnd } onMoveend={ this.handleMoveEnd }
onClick={ this.onMapClick } onMouseMove={ this.handleMove }
onMouseUp={ this.handleMouseUp }
onMouseDown={ this.onMapClick }
center={ position } center={ position }
dragging={ !trackAreaSelect }
zoom={ initialZoom || 3 } minZoom={ 2 } maxZoom={ 14 } zoom={ initialZoom || 3 } minZoom={ 2 } maxZoom={ 14 }
zoomDelta={ 0.5 } zoomDelta={ 0.5 }
bounds={ !initialZoom && initialPosition[0] && !initialPosition[1] ? mapInfo.data.bounds : initialBounds } bounds={ !initialZoom && initialPosition[0] && !initialPosition[1] ? mapInfo.data.bounds : initialBounds }
...@@ -472,6 +586,21 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -472,6 +586,21 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
</Button> </Button>
</Tooltip> </Tooltip>
</Control> </Control>
<Control position="topleft">
<Tooltip
classes={ { tooltip: classes.tooltip } }
className={ `${classes.pickRectangleButton} ${classes.mapButton}` }
title={ t(`accessions.public.p.map.${ trackClickPos ? 'stopPick' : 'selectArea'}`) }
placement="right"
>
<Button variant="outlined" onClick={ this.setAreaTrackPick }>
{ trackAreaSelect ?
<CancelIcon className={ classes.mapIcon }/> :
<PositionIcon className={ classes.mapIcon }/>
}
</Button>
</Tooltip>
</Control>
<Control position="topright"> <Control position="topright">
<div <div
onMouseEnter={ this.openLayersControls } onMouseEnter={ this.openLayersControls }
...@@ -539,6 +668,11 @@ class BrowsePage extends React.Component<IMapPageProps, any> { ...@@ -539,6 +668,11 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
</Popover> </Popover>
</div> </div>
</Control> </Control>
{ trackAreaSelect && mouseDown &&
<Rectangle bounds={ [areaStartPos, areaEndPos] }>
<div style={ {backgroundColor: 'grey', opacity: 0.4} }/>
</Rectangle>
}
<TileLayer <TileLayer
zIndex={ 0 } zIndex={ 0 }
opacity={ 0.50 } opacity={ 0.50 }
......
...@@ -31,7 +31,9 @@ export default function crop(state = INITIAL_STATE, action: IReducerAction = {ty ...@@ -31,7 +31,9 @@ export default function crop(state = INITIAL_STATE, action: IReducerAction = {ty
return updateIndex === -1 ? return updateIndex === -1 ?
update(state, { update(state, {
details: {$set: null}, details: {$set: null},
list: {$push: action.payload ? [action.payload] : []}, list: {
data: {$push: action.payload ? [action.payload] : []},
},
}) })
: :
update(state, { update(state, {
......
...@@ -51,8 +51,9 @@ function datasetsPublic(state = INITIAL_STATE, action: { type?: string, payload? ...@@ -51,8 +51,9 @@ function datasetsPublic(state = INITIAL_STATE, action: { type?: string, payload?
accessionRefs: { $set: mustRemoveAccessions ? null : state.accessionRefs }, accessionRefs: { $set: mustRemoveAccessions ? null : state.accessionRefs },
paged: { paged: {
content: { content: {
[receivedIndex]: { $set: apiCall }, [receivedIndex]: { $set: apiCall.data },
}, },
loading: {$set: false},
}, },
}); });
} else { } else {
......
...@@ -12,7 +12,7 @@ import { loadCrops } from 'crop/actions/public'; ...@@ -12,7 +12,7 @@ import { loadCrops } from 'crop/actions/public';
import DescriptorPicker from 'descriptors/ui/c/DescriptorPicker'; import DescriptorPicker from 'descriptors/ui/c/DescriptorPicker';
import { importDescriptor } from 'descriptors/actions/editor'; import { importDescriptor } from 'descriptors/actions/editor';
import { listAccessibleDescriptors, moreAccessibleDescriptors, clearDescriptors } from 'descriptors/actions/dashboard'; import { listAccessibleDescriptors, moreAccessibleDescriptors, clearDescriptors } from 'descriptors/actions/dashboard';
import Page, { IPageRequest } from 'model/Page'; import Page, { IPageRequest, SortDirection } from 'model/Page';
import Pagination from 'model/Pagination'; import Pagination from 'model/Pagination';
import { import {
loadDescriptorList, saveDescriptorList, addDescriptorsToDescriptorList, removeDescriptorsFromDescriptorList, loadDescriptorList, saveDescriptorList, addDescriptorsToDescriptorList, removeDescriptorsFromDescriptorList,
...@@ -43,7 +43,7 @@ interface IDescriptorListProps extends React.ClassAttributes<any> { ...@@ -43,7 +43,7 @@ interface IDescriptorListProps extends React.ClassAttributes<any> {
class SelectDescriptorsStep extends StepperTemplate<IDescriptorListProps> { class SelectDescriptorsStep extends StepperTemplate<IDescriptorListProps> {
protected static needs = [ protected static needs = [
({ search }) => listAccessibleDescriptors({ page: parse(search).p, size: parse(search).l, properties: parse(search).s, direction: parse(search).d }), ({ search }) => listAccessibleDescriptors({ page: +parse(search).p, size: +parse(search).l, properties: [...parse(search).s], direction: parse(search).d as SortDirection }),
]; ];
protected renderContent = () => ( protected renderContent = () => (
<DescriptorPicker <DescriptorPicker
......
...@@ -30,6 +30,7 @@ function dashboardInstitutes(state = INITIAL_STATE, action: IReducerAction) { ...@@ -30,6 +30,7 @@ function dashboardInstitutes(state = INITIAL_STATE, action: IReducerAction) {
content: { content: {
[receivedIndex]: {$set: apiCall.data}, [receivedIndex]: {$set: apiCall.data},
}, },
loading: {$set: false},
}, },
}); });
} else { } else {
......
...@@ -31,6 +31,7 @@ function publicInstitutes(state = INITIAL_STATE, action: IReducerAction) { ...@@ -31,6 +31,7 @@ function publicInstitutes(state = INITIAL_STATE, action: IReducerAction) {
content: { content: {
[receivedIndex]: {$set: apiCall.data}, [receivedIndex]: {$set: apiCall.data},
}, },
loading: {$set: false},
}, },
}); });
} else { } else {
......
...@@ -144,7 +144,7 @@ export default function admin(state = INITIAL_STATE, action: IReducerAction) { ...@@ -144,7 +144,7 @@ export default function admin(state = INITIAL_STATE, action: IReducerAction) {
// only update lastRun // only update lastRun
const {apiCall} = action.payload; const {apiCall} = action.payload;
return update(state, { return update(state, {
exec: { details: { lastRun: { $set: apiCall.data } } }, exec: { details: { lastRun: { $set: apiCall.data } } as any },
}); });
} }
case ADMIN_RECEIVE_EXEC_RUNS: { case ADMIN_RECEIVE_EXEC_RUNS: {
......
...@@ -10,7 +10,7 @@ const INITIAL_STATE: { ...@@ -10,7 +10,7 @@ const INITIAL_STATE: {
clientId: string; clientId: string;
googleClientId: string; googleClientId: string;
lang: string, lang: string,
anonToken: string, anonToken: any,
} = { } = {
frontendUrl: 'http://localhost:3000', frontendUrl: 'http://localhost:3000',
frontendPath: '', frontendPath: '',
......
...@@ -3,7 +3,7 @@ import update from 'immutability-helper'; ...@@ -3,7 +3,7 @@ import update from 'immutability-helper';
import { UPDATE_HISTORY } from 'constants/history'; import { UPDATE_HISTORY } from 'constants/history';
const INITIAL_STATE: { const INITIAL_STATE: {
hasHistory: false, hasHistory: boolean,
lastLocation: string; lastLocation: string;
} = { } = {
hasHistory: false, hasHistory: false,
......
...@@ -40,6 +40,7 @@ function publicSubsets(state = INITIAL_STATE, action: IReducerAction) { ...@@ -40,6 +40,7 @@ function publicSubsets(state = INITIAL_STATE, action: IReducerAction) {
content: { content: {
[receivedIndex]: { $set: apiCall }, [receivedIndex]: { $set: apiCall },
}, },
loading: {$set: false},
}, },
}); });
} else { } else {
......
...@@ -43,7 +43,7 @@ const mapStateToProps = (state, ownProps) => ({ ...@@ -43,7 +43,7 @@ const mapStateToProps = (state, ownProps) => ({
pageSort: parse(ownProps.location.search).s || 'lastModifiedDate', // page sort pageSort: parse(ownProps.location.search).s || 'lastModifiedDate', // page sort
pageDir: parse(ownProps.location.search).d || 'DESC', // page sort direction pageDir: parse(ownProps.location.search).d || 'DESC', // page sort direction
filterCode: parse(ownProps.location.search).filter, // filter code filterCode: parse(ownProps.location.search).filter, // filter code
filter: state.filterCode.filters && parse(ownProps.location.search).filter && state.filterCode.filters[parse(ownProps.location.search).filter] || null, filter: state.filterCode.filters && parse(ownProps.location.search).filter && state.filterCode.filters[parse(ownProps.location.search).filter as string] || null,