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 @@
"andMore": "And {{otherMore}} more",
"kml": "KML",
"filterAccessions": "Filter accessions",
"selectArea": "Select area",
"pick": "Show climate",
"stopPick": "Cancel",
"noClimateData": "No climate data available for selected location",
......
......@@ -388,9 +388,9 @@
"dev": true
},
"@types/prop-types": {
"version": "15.5.8",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.8.tgz",
"integrity": "sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw=="
"version": "15.7.1",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz",
"integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg=="
},
"@types/q": {
"version": "1.5.2",
......@@ -399,9 +399,9 @@
"dev": true
},
"@types/react": {
"version": "16.7.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.7.18.tgz",
"integrity": "sha512-Tx4uu3ppK53/iHk6VpamMP3f3ahfDLEVt3ZQc8TFm30a1H3v9lMsCntBREswZIW/SKrvJjkb3Hq8UwO6GREBng==",
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.6.tgz",
"integrity": "sha512-bN9qDjEMltmHrl0PZRI4IF2AbB7V5UlRfG+OOduckVnRQ4VzXVSzy/1eLAh778IEqhTnW0mmgL9yShfinNverA==",
"requires": {
"@types/prop-types": "*",
"csstype": "^2.2.0"
......@@ -6941,6 +6941,11 @@
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
......
......@@ -78,6 +78,7 @@
"andMore": "And {{otherMore}} more",
"kml": "KML",
"filterAccessions": "Filter accessions",
"selectArea": "Select area",
"pick": "Show climate",
"stopPick": "Cancel",
"noClimateData": "No climate data available for selected location",
......
......@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { translate } from 'react-i18next';
import { withStyles } from '@material-ui/core/styles';
import { isMobile } from 'is-mobile';
import { bindActionCreators } from 'redux';
import {showSnackbar} from 'actions/snackbar';
import navigateTo from 'actions/navigation';
......@@ -35,6 +36,7 @@ import Paper from '@material-ui/core/Paper';
import Tooltip from '@material-ui/core/Tooltip';
import LayersIcon from '@material-ui/icons/Layers';
import ClimateIcon from '@material-ui/icons/WbSunny';
import PositionIcon from '@material-ui/icons/SettingsOverscan';
import FilterIcon from '@material-ui/icons/PermDataSetting';
import CancelIcon from '@material-ui/icons/Cancel';
import ApiCall from 'model/ApiCall';
......@@ -137,6 +139,10 @@ const styles = (theme) => ({
position: 'absolute' as 'absolute',
top: '42px',
},
pickRectangleButton: {
position: 'absolute' as 'absolute',
top: '80px',
},
controlsButton: {
width: '50px',
height: '50px',
......@@ -180,11 +186,15 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
otherCount: 0,
sidebarOpened: false,
trackClickPos: false,
trackAreaSelect: false,
dialogOpened: false,
climateData: null,
layersControlsIsOpen: false,
savedControlsIsOpen: false,
colorInputIsFocused: false,
mouseDown: false,
areaStartPos: [0, 0],
areaEndPos: [0, 0],
};
constructor(props, context) {
......@@ -247,7 +257,19 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
private setPositionPick = (e) => {
e.preventDefault();
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;
}
......@@ -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 = () => {
this.setState({dialogOpened: false});
}
......@@ -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) => {
if (e.originalEvent.target.className.indexOf('leaflet-touch') === -1) {
return;
}
this.setState({mouseDown: true});
if (this.state.trackClickPos) {
return this.handleTrackPosition(e);
}
if (this.state.trackAreaSelect) {
return this.handleTrackAreaStart(e);
}
if (this.clickTimeout) {
console.log('prevented');
clearTimeout(this.clickTimeout);
......@@ -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;
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) ],
];
if (!this.inFilterBounds(searchBounds)) {
return;
}
const filterWithGeo = {
...filter,
geo: {
......@@ -350,7 +461,7 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
}
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 position = initialPosition[0] && initialPosition[1] ? initialPosition : [5, 5];
......@@ -433,13 +544,16 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
displayName="accessions.common.modelName"
amount={ mapInfo.data.accessionCount }
/>
<div className={ `${classes.leafletContainer} ${trackClickPos && classes.crosshair}` }>
<div className={ `${classes.leafletContainer} ${(trackClickPos || trackAreaSelect) && classes.crosshair}` }>
{ loading && <Loading /> }
{ mapInfo && typeof window !== 'undefined' &&
<MapComponent
onMoveend={ this.handleMoveEnd }
onClick={ this.onMapClick }
onMouseMove={ this.handleMove }
onMouseUp={ this.handleMouseUp }
onMouseDown={ this.onMapClick }
center={ position }
dragging={ !trackAreaSelect }
zoom={ initialZoom || 3 } minZoom={ 2 } maxZoom={ 14 }
zoomDelta={ 0.5 }
bounds={ !initialZoom && initialPosition[0] && !initialPosition[1] ? mapInfo.data.bounds : initialBounds }
......@@ -472,6 +586,21 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
</Button>
</Tooltip>
</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">
<div
onMouseEnter={ this.openLayersControls }
......@@ -539,6 +668,11 @@ class BrowsePage extends React.Component<IMapPageProps, any> {
</Popover>
</div>
</Control>
{ trackAreaSelect && mouseDown &&
<Rectangle bounds={ [areaStartPos, areaEndPos] }>
<div style={ {backgroundColor: 'grey', opacity: 0.4} }/>
</Rectangle>
}
<TileLayer
zIndex={ 0 }
opacity={ 0.50 }
......
......@@ -31,7 +31,9 @@ export default function crop(state = INITIAL_STATE, action: IReducerAction = {ty
return updateIndex === -1 ?
update(state, {
details: {$set: null},
list: {$push: action.payload ? [action.payload] : []},
list: {
data: {$push: action.payload ? [action.payload] : []},
},
})
:
update(state, {
......
......@@ -51,8 +51,9 @@ function datasetsPublic(state = INITIAL_STATE, action: { type?: string, payload?
accessionRefs: { $set: mustRemoveAccessions ? null : state.accessionRefs },
paged: {
content: {
[receivedIndex]: { $set: apiCall },
[receivedIndex]: { $set: apiCall.data },
},
loading: {$set: false},
},
});
} else {
......
......@@ -12,7 +12,7 @@ import { loadCrops } from 'crop/actions/public';
import DescriptorPicker from 'descriptors/ui/c/DescriptorPicker';
import { importDescriptor } from 'descriptors/actions/editor';
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 {
loadDescriptorList, saveDescriptorList, addDescriptorsToDescriptorList, removeDescriptorsFromDescriptorList,
......@@ -43,7 +43,7 @@ interface IDescriptorListProps extends React.ClassAttributes<any> {
class SelectDescriptorsStep extends StepperTemplate<IDescriptorListProps> {
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 = () => (
<DescriptorPicker
......
......@@ -30,6 +30,7 @@ function dashboardInstitutes(state = INITIAL_STATE, action: IReducerAction) {
content: {
[receivedIndex]: {$set: apiCall.data},
},
loading: {$set: false},
},
});
} else {
......
......@@ -31,6 +31,7 @@ function publicInstitutes(state = INITIAL_STATE, action: IReducerAction) {
content: {
[receivedIndex]: {$set: apiCall.data},
},
loading: {$set: false},
},
});
} else {
......
......@@ -144,7 +144,7 @@ export default function admin(state = INITIAL_STATE, action: IReducerAction) {
// only update lastRun
const {apiCall} = action.payload;
return update(state, {
exec: { details: { lastRun: { $set: apiCall.data } } },
exec: { details: { lastRun: { $set: apiCall.data } } as any },
});
}
case ADMIN_RECEIVE_EXEC_RUNS: {
......
......@@ -10,7 +10,7 @@ const INITIAL_STATE: {
clientId: string;
googleClientId: string;
lang: string,
anonToken: string,
anonToken: any,
} = {
frontendUrl: 'http://localhost:3000',
frontendPath: '',
......
......@@ -3,7 +3,7 @@ import update from 'immutability-helper';
import { UPDATE_HISTORY } from 'constants/history';
const INITIAL_STATE: {
hasHistory: false,
hasHistory: boolean,
lastLocation: string;
} = {
hasHistory: false,
......
......@@ -40,6 +40,7 @@ function publicSubsets(state = INITIAL_STATE, action: IReducerAction) {
content: {
[receivedIndex]: { $set: apiCall },
},
loading: {$set: false},
},
});
} else {
......
......@@ -43,7 +43,7 @@ const mapStateToProps = (state, ownProps) => ({
pageSort: parse(ownProps.location.search).s || 'lastModifiedDate', // page sort
pageDir: parse(ownProps.location.search).d || 'DESC', // page sort direction
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,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
......
......@@ -122,7 +122,7 @@ const mapStateToProps = (state, ownProps) => ({
pageSort: parse(ownProps.location.search).s, // page sort
pageDir: parse(ownProps.location.search).d, // page sort direction
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,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
......
......@@ -96,13 +96,21 @@ export default function user(state = INITIAL_STATE, action: IReducerAction) {
}
case ADMIN_RECEIVE_OAUTH_CLIENT: {
const { apiCall } = action.payload;
return update(state, {
return state.oAuthClient ? update(state, {
oAuthClient: {
$set: {
data: {
clientDetails: {$set: apiCall.data},
},
},
})
: update(state, {
oAuthClient: {
$set: {
...apiCall,
data: {
clientDetails: apiCall.data,
},
},
},
});
}
......
......@@ -51,11 +51,11 @@ class EditOAuthClientPage extends React.Component<IEditPageProps, any> {
public render() {
const { oAuthClient, t } = this.props;
const clientDetails = oAuthClient && oAuthClient.clientDetails !== null ? oAuthClient.clientDetails : new OAuthClient();
const clientDetails = oAuthClient && oAuthClient.clientDetails ? oAuthClient.clientDetails : new OAuthClient();
return clientDetails === null ? (<Loading/>) : (
<div>
<ContentHeaderWithButton title={ clientDetails.title || t('user.admin.p.oAuthEdit.createNew') }/>
<ContentHeaderWithButton title={ clientDetails && clientDetails.title || t('user.admin.p.oAuthEdit.createNew') }/>
<PageContents className="pt-1rem">
<Card>
<CardContent>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment