MapPage.tsx 12.5 KB
Newer Older
1
import * as React from 'react';
2
import { connect } from 'react-redux';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
3
import { Link } from 'react-router-dom';
4
5
6
import { translate } from 'react-i18next';
import { withStyles } from '@material-ui/core/styles';
import { bindActionCreators } from 'redux';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
7
import {showSnackbar} from 'actions/snackbar';
8
import { loadAccessionsMapInfo } from 'accessions/actions/public';
9
import AccessionFilter from 'model/accession/AccessionFilter';
10
import Loading from 'ui/common/Loading';
11
import AccessionMapInfo from 'model/accession/AccessionMapInfo';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
12
import MapLayer from 'model/genesys/MapTileLayer';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
13
import ContentHeader from 'ui/common/heading/ContentHeader';
14
import Button from '@material-ui/core/Button';
15
import Tabs, { Tab } from 'ui/common/Tabs';
16
import PrettyFilters from 'ui/common/filter/PrettyFilters';
Oleksii Savran's avatar
Oleksii Savran committed
17
import ButtonBar from 'ui/common/buttons/ButtonBar';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
18
19
import ContentLayout from 'ui/layout/ContentLayout';
import MapConfigSection from './c/MapConfigSection';
20

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
21
import ClimateService from 'service/genesys/ClimateService';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
22
import AccessionService from 'service/genesys/AccessionService';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
23
24
25
import MapAccessionsFilters from 'accessions/ui/c/MapAccessionsFilters';
import BioClimateDisplay from 'accessions/ui/c/BioClimateDisplay';
import {Dialog, Drawer} from '@material-ui/core';
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
26
import PageTitle from 'ui/common/PageTitle';
27
28
29

let Map;
let TileLayer;
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
30
let Popup;
Oleksii Savran's avatar
Oleksii Savran committed
31
let Rectangle;
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
32
33

const popupContentLimit = 11;
34

35
36
37
38
interface IMapPageProps {
    classes?: any;
    t?: any;

39
    apiUrl: string;
40
    mapInfo: AccessionMapInfo;
41
42
    currentTab: string;
    filterCode: string;
43
    loadAccessionsMapInfo: any;
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
44
    mapLayers: MapLayer[];
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
45
    showSnackbar: (message: string) => void;
46
47
48
49
50
51
52
53
}

const styles = (theme) => ({
    leafletContainer: {
        width: '100%',
        minHeight: '500px',
        height: 'calc(100vh - 186px)',
    },
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
    crosshair: {
        '& > div': {
            cursor: 'crosshair !important' as 'crosshair !important',
        },
    },
    climateDialog: {
        marginBottom: 0,
        overflow: 'auto' as 'auto',
    },
    filterAccessionsButton: {
        position: 'absolute' as 'absolute',
        zIndex: 1000,
        top: '8px',
        left: '48px',
    },
    pickPositionButton: {
        position: 'absolute' as 'absolute',
        zIndex: 1000,
        top: '48px',
        left: '48px',
    },
75
76
77
});

class BrowsePage extends React.Component<IMapPageProps, any> {
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
78
79
80

    private clickTimeout;

81
    protected static needs = [
82
83
      ({ params: { filterCode } }) => {
        return loadAccessionsMapInfo(filterCode || '');
84
85
86
      },
    ];

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
87
    public state = {
Oleksii Savran's avatar
Oleksii Savran committed
88
89
      clickLocation: [],
      searchBox: null,
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
90
91
      geoData: [],
      otherCount: 0,
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
92
93
94
95
      sidebarOpened: false,
      trackClickPos: false,
      dialogOpened: false,
      climateData: null,
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
96
97
    };

98
99
100
101
102
    constructor(props, context) {
        super(props, context);
        if (typeof window !== 'undefined') {
            Map = require('react-leaflet').Map;
            TileLayer = require('react-leaflet').TileLayer;
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
103
            Popup = require('react-leaflet').Popup;
Oleksii Savran's avatar
Oleksii Savran committed
104
            Rectangle = require('react-leaflet').Rectangle;
105
106
107
108
109
        }
    }


    public componentWillMount() {
110
111
112
113
114
115
116
      const { mapInfo, filterCode, loadAccessionsMapInfo } = this.props;
      // console.log(`Filter code for map ${filterCode} ?== ${mapInfo && mapInfo.filterCode}`, filterCode, mapInfo ? mapInfo.filterCode : 'No mapInfo');
      if (mapInfo && mapInfo.filterCode !== filterCode) {
        // console.log(`mapInfo.filterCode !== filterCode. updatingRoute`);
        loadAccessionsMapInfo(filterCode || '');
      } else if (!mapInfo) {
        loadAccessionsMapInfo(filterCode || '');
117
118
119
      }
    }

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
    private openSidebar = () => {
      return this.setState({sidebarOpened: true});
    }

    private closeSidebar = () => {
      return this.setState({sidebarOpened: false});
    }

    private setPositionPick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      this.setState({trackClickPos: !this.state.trackClickPos});
      return false;
    }

    private handleTrackPosition = (e) => {
      const {showSnackbar, t} = this.props;
      const { lat, lng } = e.latlng;

      this.setState({climateData: null});

      ClimateService.getCurrentClimate(lat, lng)
        .then((res) => {
          // @ts-ignore
          if (!res || res === '') {
            return showSnackbar(t('accessions.public.p.map.noClimateData'));
          }
          return this.setState({climateData: res, dialogOpened: true});
        })
        .catch((error) =>  {
          if (error.code === 404) {
            return showSnackbar(t('accessions.public.p.map.noClimateData'));
          }
        });
    }

    private hideDialog = () => {
      this.setState({dialogOpened: false});
    }
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
159
160

    private onMapClick = (e) => {
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
161
162
163
164
165
166
167
168
169

        if (e.originalEvent.target.className.indexOf('leaflet-touch') === -1) {
          return;
        }

        if (this.state.trackClickPos) {
          return this.handleTrackPosition(e);
        }

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
        if (this.clickTimeout) {
            console.log('prevented');
            clearTimeout(this.clickTimeout);
            this.clickTimeout = null;
            return;
        }
        this.clickTimeout = setTimeout(() => {
            console.log('started');
            this.clickTimeout = null;
            const currentZoom = e.target._zoom;
            if (currentZoom < 4) {
                return;
            }

            const {mapInfo: {filter}} = this.props;

            const correctLng = Math.abs(e.latlng.lng) > 180 ? (360 + e.latlng.lng) % 180 : e.latlng.lng;

            console.log(e);

Oleksii Savran's avatar
Oleksii Savran committed
190
191
192
193
194
195
            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) ],
            ];
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
196
197
198
199
            const filterWithGeo = {
                ...filter,
                geo: {
                    longitude: {
Oleksii Savran's avatar
Oleksii Savran committed
200
201
                        ge: searchBounds[0][1],
                        le: searchBounds[1][1],
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
202
203
                    },
                    latitude: {
Oleksii Savran's avatar
Oleksii Savran committed
204
205
                        ge: searchBounds[0][0],
                        le: searchBounds[1][0],
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
206
                    },
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
207
                    climate: filter && filter.geo && filter.geo.climate,
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
208
209
210
                },
            };
            AccessionService.geoJson(filterWithGeo, popupContentLimit)
Oleksii Savran's avatar
Oleksii Savran committed
211
                .then((res) => this.setState({clickLocation: [ e.latlng.lat,  e.latlng.lng], searchBox: searchBounds, geoData: res.geoJson, otherCount: res.otherCount}));
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
212
213
214
        }, 200);
    }

215
216
217
218
219
    /// Wrap loadAccessionsMapInfo dispatch and fills the current sort selection
    protected myApplyFilters = (filters: AccessionFilter) => {
      const { loadAccessionsMapInfo } = this.props;

      loadAccessionsMapInfo(filters);
220
221
222
    }

    public render() {
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
223
        const { searchBox, geoData, otherCount, sidebarOpened, trackClickPos, dialogOpened, climateData} = this.state;
224
225
        const position = [30, 0];

Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
226
        const { mapInfo, mapLayers, currentTab, classes, filterCode, t, loadAccessionsMapInfo } = this.props;
227
228
229
230

        if (! mapInfo) {
          return <Loading />;
        }
231
232

        // const color = 'f00ba0';
233
        const layerUrl = `{s}/acn/tile/{z}/{x}/{y}?f=${filterCode ? filterCode : ''}`; // `&color=${color}`;
234
235

        return (
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
236
237
238
239
240
            <ContentLayout
              right={ <MapConfigSection/> }
              rightAlwaysCollapsible
              customHeaderHeight
            >
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
241
242
              <PageTitle title={ t('accessions.public.p.browse.title') } />
              <ContentHeader title={ t('accessions.public.p.browse.title') } subTitle={ t('accessions.public.p.browse.subTitle') } />
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
243
244
245
246
247
248
249
250
251
252
253

              <Drawer variant="temporary" open={ sidebarOpened } onClose={ this.closeSidebar }>
                    <MapAccessionsFilters onSubmit={ loadAccessionsMapInfo } initialValues={ mapInfo && mapInfo.filter }/>
                </Drawer>


              <Dialog open={ dialogOpened } onClose={ this.hideDialog } maxWidth="md" fullWidth>
                <BioClimateDisplay classes={ {section: classes.climateDialog} } climateData={ climateData }/>
              </Dialog>

                <Tabs
254
255
                    tab={ currentTab }
                    actions={
Oleksii Savran's avatar
Oleksii Savran committed
256
                      <ButtonBar>
257
                        <span>
Matija Obreza's avatar
Matija Obreza committed
258
259
                          <form method="post" action="/proxy/api/v1/acn/downloadKml">
                            <input type="hidden" name="f" value={ filterCode } />
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
260
                            <Button type="submit">{ `${t('common:action.download')} ${t('accessions.public.p.map.kml')}` }</Button>
Matija Obreza's avatar
Matija Obreza committed
261
                          </form>
262
                        </span>
Oleksii Savran's avatar
Oleksii Savran committed
263
                      </ButtonBar>
264
265
                    }
                >
Viacheslav Pavlov's avatar
i18n    
Viacheslav Pavlov committed
266
267
268
                  <Tab name="data" to={ `/a/${filterCode || ''}` }>{ t('accessions.tab.data') }</Tab>
                  <Tab name="overview" to={ `/a/overview/${filterCode || ''}` }>{ t('accessions.tab.overview') }</Tab>
                  <Tab name="map" to={ `/a/map/${filterCode || '' }` }>{ t('accessions.tab.map') }</Tab>
269
270
271
                </Tabs>
                <PrettyFilters
                    prefix="accessions"
272
                    filterObj={ mapInfo && mapInfo.filter || {} }
273
274
                    onSubmit={ this.myApplyFilters }
                />
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
275
276
                <div className={ `${classes.leafletContainer} ${trackClickPos && classes.crosshair}` }>
                 { mapInfo && typeof window !== 'undefined' &&
277
                    <Map
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
278
                        onClick={ this.onMapClick }
279
                        center={ position }
280
                        zoom={ 3 } minZoom={ 2 } maxZoom={ 14 }
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
281
282
283
284
285
                        bounds={ mapInfo.bounds }
                    >
                      <Button variant="contained" className={ classes.filterAccessionsButton } onClick={ this.openSidebar }>{ t(`accessions.public.p.map.filterAccessions`) }</Button>
                      <Button variant="contained" className={ classes.pickPositionButton } onClick={ this.setPositionPick }>{ t(`accessions.public.p.map.${ trackClickPos ? 'stopPick' : 'pick'}`) }</Button>
                      <TileLayer
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
286
                            zIndex={ 0 }
287
288
289
290
                            opacity={ 0.50 }
                            attribution={ '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }
                            url={ 'https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png' }
                        />
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
291
292
293

                        { mapLayers && mapLayers.filter((layer) => layer.enabled).map((layer, index) => <TileLayer zIndex={ index + 1 } key={ layer.name } { ...layer }/>) }

Matija Obreza's avatar
Matija Obreza committed
294
                        <TileLayer
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
295
                            zIndex={ mapLayers.length + 1 }
296
297
298
299
                            updateInterval={ 1000 }
                            updateWhenZooming={ false }
                            attribution="&amp;copy Accession localities from <a href=&quot;/&quot;>Genesys PGR</a>"
                            url={ layerUrl }
300
                            subdomains={ mapInfo.tileServers }
301
                        />
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
302
                        { geoData && geoData.length > 0 &&
Oleksii Savran's avatar
Oleksii Savran committed
303
304
305
306
                            <Rectangle
                              bounds={ searchBox }
                              ref={ (marker) => marker && marker.leafletElement.openPopup() }
                            >
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
307
308
                                <Popup open>
                                    <div>
309
                                        { geoData.map((feature, idx) => (<div key={ idx }><Link to={ `/a/${feature.properties.uuid}` }>{ `${feature.properties.accessionNumber} ${feature.properties.instCode}` }</Link></div>)) }
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
310
311
312
                                        { otherCount > 0 && <div>{ t('accessions.public.p.map.andMore', {otherMore: otherCount}) }</div> }
                                    </div>
                                </Popup>
Oleksii Savran's avatar
Oleksii Savran committed
313
                            </Rectangle>
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
314
                        }
315
316
317
                    </Map>
                }
                </div>
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
318
            </ContentLayout>
319
320
321
322
323
        );
    }
}

const mapStateToProps = (state, ownProps) => ({
324
    mapInfo: state.accessions.public.mapInfo || undefined,
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
325
    mapLayers: state.accessions.public.mapLayers,
326
327
328
329
330
    filterCode: ownProps.match.params.filterCode || '',
    currentTab: ownProps.match.params.tab || 'map', // current tab, or ownProps.location.pathname
});

const mapDispatchToProps = (dispatch) => bindActionCreators({
331
    loadAccessionsMapInfo,
Viacheslav Pavlov's avatar
Viacheslav Pavlov committed
332
    showSnackbar,
333
334
}, dispatch);

335
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(translate()(BrowsePage)));