Skip to content

Commit 9b57167

Browse files
authored
Merge pull request #1860 from DrAcula27/add-mapbox-zoom-1793-v2
Add mapbox zoom 1793 v2
2 parents 1b6b219 + 6b9eaed commit 9b57167

File tree

4 files changed

+197
-51
lines changed

4 files changed

+197
-51
lines changed

components/Map/Map.jsx

Lines changed: 151 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/* eslint-disable */
22

33
import React from 'react';
4+
import ReactDOM from 'react-dom';
45
import PropTypes from 'prop-types';
56
import { connect } from 'react-redux';
67
import withStyles from '@mui/styles/withStyles';
78
import mapboxgl from 'mapbox-gl';
89
import FilterMenu from '@components/main/Desktop/FilterMenu';
10+
import Tooltip from '@mui/material/Tooltip';
911
// import LocationDetail from './LocationDetail';
1012
import { REQUEST_TYPES } from '@components/common/CONSTANTS';
1113
import { getNcByLngLat, clearPinInfo } from '@reducers/data';
@@ -49,6 +51,8 @@ import RequestDetail from './RequestDetail';
4951
import { debounce, isEmpty } from '@utils';
5052

5153
import settings from '@settings';
54+
import ZoomTooltip from './zoomTooltip';
55+
import { DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM } from '@components/common/CONSTANTS';
5256

5357
const styles = (theme) => ({
5458
root: {
@@ -60,9 +64,8 @@ const styles = (theme) => ({
6064
'& canvas.mapboxgl-canvas:focus': {
6165
outline: 'none',
6266
},
63-
// TODO: controls placement
6467
'& .mapboxgl-control-container': {
65-
display: 'none',
68+
// TODO: update styles here when design finalized
6669
},
6770
'& .mapboxgl-popup-content': {
6871
width: 'auto',
@@ -100,7 +103,7 @@ const styles = (theme) => ({
100103
});
101104

102105
// Define feature layers
103-
const featureLayers = ['request-circles','nc-fills'];
106+
const featureLayers = ['request-circles', 'nc-fills'];
104107

105108
class Map extends React.Component {
106109
// Note: 'this.context' is defined using the static contextType property
@@ -151,12 +154,22 @@ class Map extends React.Component {
151154
pitchWithRotate: false,
152155
dragRotate: false,
153156
touchZoomRotate: false,
157+
minZoom: DEFAULT_MIN_ZOOM,
158+
maxZoom: DEFAULT_MAX_ZOOM,
154159
});
155160

156161
map.on('load', () => {
157162
if (this.isSubscribed) {
158163
this.initLayers(true);
159164

165+
map.addControl(
166+
new mapboxgl.NavigationControl({
167+
visualizePitch: false,
168+
showCompass: false,
169+
}),
170+
'bottom-right'
171+
);
172+
160173
map.on('click', this.debouncedOnClick);
161174
map.on('mouseenter', 'request-circles', this.onMouseEnter);
162175
map.on('mouseleave', 'request-circles', this.onMouseLeave);
@@ -178,16 +191,20 @@ class Map extends React.Component {
178191
const entireMapLoadTime = () => {
179192
if (this.map.isSourceLoaded('requests')) {
180193
const { dbStartTime } = this.context;
181-
const pinLoadEndTime = performance.now()
182-
console.log(`Pin load time: ${Math.floor(pinLoadEndTime - dbStartTime)} ms`)
194+
const pinLoadEndTime = performance.now();
195+
console.log(
196+
`Pin load time: ${Math.floor(
197+
pinLoadEndTime - dbStartTime
198+
)} ms`
199+
);
183200
this.map.off('idle', entireMapLoadTime);
184201
}
185-
}
186-
202+
};
203+
187204
if (this.props.requests != prevProps.requests) {
188205
if (this.state.mapReady) {
189206
this.setState({ requests: this.props.requests });
190-
this.map.on('idle', entireMapLoadTime)
207+
this.map.on('idle', entireMapLoadTime);
191208
// this.map.once('idle', this.setFilteredRequestCounts);
192209
} else {
193210
this.map.once('idle', () => {
@@ -203,6 +220,50 @@ class Map extends React.Component {
203220
// });
204221
// }
205222
this.map.on('load', () => {
223+
// grab the Zoom Out button of the Mapbox zoom controls
224+
const zoomOutControl = document.querySelector(
225+
'.mapboxgl-ctrl-zoom-out'
226+
);
227+
228+
// use state to control tooltip's visibility
229+
let showZoomTooltip = false;
230+
231+
// if zoom controls aren't limited, add the 'Zoom out' title back
232+
if (!showZoomTooltip) {
233+
zoomOutControl.title = 'Zoom out';
234+
}
235+
236+
// render the zoomtooltip component
237+
const renderZoomTooltip = () => {
238+
ReactDOM.render(
239+
<ZoomTooltip show={showZoomTooltip} />,
240+
zoomOutControl
241+
);
242+
};
243+
244+
// show the zoomtooltip on hover if the map is locked onto an ncLayer AND
245+
// the zoom out control is disabled
246+
const handleMouseEnter = () => {
247+
// check if the current zoom level (this.map.getZoom()) is at or below minZoom,
248+
// indicating the map is zoomed in to its minimum level & the zoom out control is disabled
249+
const isZoomOutDisabled =
250+
this.map.getZoom() <= this.state.minZoom;
251+
if (this.state.filterGeo && isZoomOutDisabled) {
252+
showZoomTooltip = true;
253+
renderZoomTooltip();
254+
}
255+
};
256+
257+
// hide the zoomtooltip on mouse leave
258+
const handleMouseLeave = () => {
259+
showZoomTooltip = false;
260+
renderZoomTooltip();
261+
};
262+
263+
// add hover event listeners to the zoomOutControl
264+
zoomOutControl.addEventListener('mouseenter', handleMouseEnter);
265+
zoomOutControl.addEventListener('mouseleave', handleMouseLeave);
266+
206267
if (
207268
this.state.filterGeo !== prevState.filterGeo ||
208269
this.state.selectedTypes !== prevState.selectedTypes
@@ -216,6 +277,7 @@ class Map extends React.Component {
216277
sourceId: 'nc',
217278
sourceData: this.props.ncBoundaries,
218279
idProperty: 'NC_ID',
280+
219281
onSelectRegion: (geo) => {
220282
this.setState({
221283
locationInfo: {
@@ -228,7 +290,17 @@ class Map extends React.Component {
228290
},
229291
});
230292
this.map.once('zoomend', () => {
231-
this.setState({ filterGeo: geo });
293+
this.setState((prevState) => {
294+
const newMinZoom = this.map.getZoom();
295+
this.map.setMinZoom(newMinZoom);
296+
return {
297+
filterGeo: geo,
298+
minZoom: newMinZoom,
299+
};
300+
});
301+
302+
// initial render
303+
renderZoomTooltip();
232304
});
233305
},
234306
onHoverRegion: (geo) => {
@@ -257,12 +329,16 @@ class Map extends React.Component {
257329
ncBoundaries
258330
) {
259331
try {
260-
const selectedCouncilId = Number(this.initialState.councilId);
332+
const selectedCouncilId = Number(
333+
this.initialState.councilId
334+
);
261335
const newSelectedCouncil = councils.find(
262336
({ councilId }) => councilId === selectedCouncilId
263337
);
264338
if (!newSelectedCouncil) {
265-
throw new Error('Council Does not exist from search query');
339+
throw new Error(
340+
'Council Does not exist from search query'
341+
);
266342
}
267343
const newSelected = [newSelectedCouncil];
268344
dispatchUpdateSelectedCouncils(newSelected);
@@ -278,7 +354,9 @@ class Map extends React.Component {
278354

279355
if (this.props.selectedNcId !== prevProps.selectedNcId) {
280356
const { councils, selectedNcId } = this.props;
281-
const nc = councils.find(({ councilId }) => councilId === selectedNcId);
357+
const nc = councils.find(
358+
({ councilId }) => councilId === selectedNcId
359+
);
282360
this.setState({ selectedNc: nc });
283361
return this.ncLayer.selectRegion(selectedNcId);
284362
}
@@ -301,9 +379,9 @@ class Map extends React.Component {
301379
...(center
302380
? {
303381
locationInfo: {
304-
location: `${center.lat.toFixed(6)} N ${center.lng.toFixed(
382+
location: `${center.lat.toFixed(
305383
6
306-
)} E`,
384+
)} N ${center.lng.toFixed(6)} E`,
307385
radius: 1,
308386
nc: ncInfoFromLngLat(center),
309387
},
@@ -334,7 +412,9 @@ class Map extends React.Component {
334412
},
335413
onHoverRegion: (geo) => {
336414
this.setState({
337-
hoveredRegionName: geo ? ccNameFromId(geo.properties.name) : null,
415+
hoveredRegionName: geo
416+
? ccNameFromId(geo.properties.name)
417+
: null,
338418
});
339419
},
340420
});
@@ -381,6 +461,9 @@ class Map extends React.Component {
381461
canReset: true,
382462
});
383463
});
464+
465+
// Reset MinZoom to original value after deselecting NC
466+
this.map.setMinZoom(DEFAULT_MIN_ZOOM);
384467
};
385468

386469
resetBoundaries = () => {
@@ -400,7 +483,8 @@ class Map extends React.Component {
400483
};
401484

402485
addressSearchIsEmpty = () => {
403-
const addressSearchInput = document.querySelector('#geocoder input');
486+
const addressSearchInput =
487+
document.querySelector('#geocoder input');
404488
return !Boolean(addressSearchInput?.value?.trim());
405489
};
406490

@@ -440,7 +524,8 @@ class Map extends React.Component {
440524
// Display pop-ups only for the current district
441525
if (
442526
features[i].properties.council_id &&
443-
this.props.selectedNcId !== features[i].properties.council_id
527+
this.props.selectedNcId !==
528+
features[i].properties.council_id
444529
) {
445530
return;
446531
}
@@ -456,7 +541,7 @@ class Map extends React.Component {
456541
};
457542

458543
onMouseLeave = (e) => {
459-
this.props.dispatchClearPinInfo()
544+
this.props.dispatchClearPinInfo();
460545
this.removePopup();
461546
};
462547

@@ -473,26 +558,26 @@ class Map extends React.Component {
473558
const features = this.getAllFeaturesAtPoint(e.point);
474559
for (let i = 0; i < features.length; i += 1) {
475560
const feature = features[i];
476-
if (feature.layer.id == "nc-fills") {
477-
this.setState({ address: null });
561+
if (feature.layer.id == 'nc-fills') {
562+
this.setState({ address: null });
478563

479-
this.resetAddressSearch(); // Clear address search input
480-
dispatchCloseBoundaries(); // Collapse boundaries section
564+
this.resetAddressSearch(); // Clear address search input
565+
dispatchCloseBoundaries(); // Collapse boundaries section
481566

482-
const selectedCouncilId = Number(feature.properties.NC_ID);
483-
const newSelectedCouncil = councils.find(
484-
({ councilId }) => councilId === selectedCouncilId
485-
);
486-
const newSelected = isEmpty(newSelectedCouncil)
487-
? null
488-
: [newSelectedCouncil];
567+
const selectedCouncilId = Number(feature.properties.NC_ID);
568+
const newSelectedCouncil = councils.find(
569+
({ councilId }) => councilId === selectedCouncilId
570+
);
571+
const newSelected = isEmpty(newSelectedCouncil)
572+
? null
573+
: [newSelectedCouncil];
489574

490-
dispatchUpdateSelectedCouncils(newSelected);
491-
dispatchUpdateUnselectedCouncils(councils);
492-
dispatchUpdateNcId(selectedCouncilId);
493-
494-
return this.ncLayer.selectRegion(feature.id);
495-
} else{
575+
dispatchUpdateSelectedCouncils(newSelected);
576+
dispatchUpdateUnselectedCouncils(councils);
577+
dispatchUpdateNcId(selectedCouncilId);
578+
579+
return this.ncLayer.selectRegion(feature.id);
580+
} else {
496581
return null;
497582
}
498583
}
@@ -514,7 +599,7 @@ class Map extends React.Component {
514599
dispatchCloseBoundaries,
515600
dispatchUpdateSelectedCouncils,
516601
dispatchUpdateUnselectedCouncils,
517-
councils
602+
councils,
518603
} = this.props;
519604

520605
// Reset boundaries input
@@ -532,21 +617,29 @@ class Map extends React.Component {
532617
} else {
533618
// When result.properties.type does not equal "District"
534619
const [longitude, latitude] = result.center;
535-
const address = result.place_name.split(',').slice(0, -2).join(', ');
536-
537-
const ncIdOfAddressSearch = getNcByLngLatv2({ longitude, latitude });
620+
const address = result.place_name
621+
.split(',')
622+
.slice(0, -2)
623+
.join(', ');
624+
625+
const ncIdOfAddressSearch = getNcByLngLatv2({
626+
longitude,
627+
latitude,
628+
});
538629
if (!isEmpty(ncIdOfAddressSearch)) {
539-
//Adding name pill to search bar
630+
//Adding name pill to search bar
540631
const newSelectedCouncil = councils.find(
541-
({ councilId }) => councilId === ncIdOfAddressSearch,
632+
({ councilId }) => councilId === ncIdOfAddressSearch
542633
);
543634
if (!newSelectedCouncil) {
544-
throw new Error('Council Id in address search geocoder result could not be found');
635+
throw new Error(
636+
'Council Id in address search geocoder result could not be found'
637+
);
545638
}
546639
const newSelected = [newSelectedCouncil];
547640
dispatchUpdateSelectedCouncils(newSelected);
548641
dispatchUpdateUnselectedCouncils(councils);
549-
642+
550643
dispatchUpdateNcId(Number(ncIdOfAddressSearch));
551644
this.setState({
552645
address: address,
@@ -562,7 +655,7 @@ class Map extends React.Component {
562655
center: [longitude, latitude],
563656
essential: true,
564657
zoom: 9,
565-
});
658+
});
566659
return this.addressLayer.addMarker([longitude, latitude]);
567660
}
568661
}
@@ -613,15 +706,19 @@ class Map extends React.Component {
613706
}
614707
})();
615708

616-
return Object.keys(counts[regionId]).reduce((filteredCounts, rType) => {
617-
if (selectedTypes.includes(rType))
618-
filteredCounts[rType] = counts[regionId][rType];
619-
return filteredCounts;
620-
}, {});
709+
return Object.keys(counts[regionId]).reduce(
710+
(filteredCounts, rType) => {
711+
if (selectedTypes.includes(rType))
712+
filteredCounts[rType] = counts[regionId][rType];
713+
return filteredCounts;
714+
},
715+
{}
716+
);
621717
};
622718

623719
setFilteredRequestCounts = () => {
624-
const { requests, filterGeo, geoFilterType, selectedTypes } = this.state;
720+
const { requests, filterGeo, geoFilterType, selectedTypes } =
721+
this.state;
625722
const { ncCounts, ccCounts } = this.props;
626723

627724
// use pre-calculated values for nc and cc filters if available
@@ -695,7 +792,10 @@ class Map extends React.Component {
695792
const { classes } = this.props;
696793

697794
return (
698-
<div className={classes.root} ref={(el) => (this.mapContainer = el)}>
795+
<div
796+
className={classes.root}
797+
ref={(el) => (this.mapContainer = el)}
798+
>
699799
<RequestsLayer
700800
ref={(el) => (this.requestsLayer = el)}
701801
activeLayer={activeRequestsLayer}

0 commit comments

Comments
 (0)