Skip to content

Commit 88b1782

Browse files
authored
Merge branch 'main' into feat/complent-form
2 parents 6d818f9 + f24971a commit 88b1782

5 files changed

Lines changed: 184 additions & 9 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { RefObject, useEffect } from 'react'
2+
3+
export function useConstrainedFloatingButton(
4+
mapContainerRef: RefObject<HTMLDivElement | null>,
5+
buttonRef: RefObject<HTMLButtonElement | null>,
6+
isExpanded: boolean,
7+
) {
8+
useEffect(() => {
9+
const updateButtonPosition = () => {
10+
if (!mapContainerRef.current || !buttonRef.current) return
11+
12+
const mapRect = mapContainerRef.current.getBoundingClientRect()
13+
const buttonRect = buttonRef.current.getBoundingClientRect()
14+
const buttonHeight = buttonRect.height || 48
15+
const offset = 5
16+
17+
const buttonElement = buttonRef.current
18+
19+
if (isExpanded) {
20+
if (buttonElement) {
21+
buttonElement.style.left = `${offset}px`
22+
buttonElement.style.bottom = `${3 * offset + offset}px`
23+
buttonElement.style.top = ''
24+
buttonElement.style.display = ''
25+
}
26+
return
27+
}
28+
29+
const mapTop = mapRect.top
30+
const mapBottom = mapRect.bottom
31+
const mapVisible = mapBottom > 0 && mapTop < window.innerHeight
32+
33+
if (!mapVisible) {
34+
if (buttonElement) {
35+
buttonElement.style.display = 'none'
36+
}
37+
return
38+
}
39+
40+
if (buttonElement) {
41+
buttonElement.style.display = ''
42+
}
43+
44+
const desiredBottomFromViewportBottom = offset
45+
const desiredTopFromViewportTop = window.innerHeight - buttonHeight - offset
46+
const desiredBottomFromViewportTop = window.innerHeight - desiredBottomFromViewportBottom
47+
48+
let finalTop: number | undefined
49+
let finalBottom: number | undefined
50+
51+
const buttonTopFromViewportTop = desiredTopFromViewportTop
52+
const buttonBottomFromViewportTop = desiredBottomFromViewportTop
53+
54+
if (buttonTopFromViewportTop < mapTop) {
55+
finalTop = mapTop + offset
56+
finalBottom = undefined
57+
} else if (buttonBottomFromViewportTop > mapBottom) {
58+
const constrainedBottomFromTop = mapBottom - offset
59+
if (constrainedBottomFromTop - buttonHeight < mapTop) {
60+
finalTop = mapTop + offset
61+
finalBottom = undefined
62+
} else {
63+
finalBottom = window.innerHeight - constrainedBottomFromTop
64+
finalTop = undefined
65+
}
66+
} else {
67+
finalBottom = desiredBottomFromViewportBottom
68+
finalTop = undefined
69+
}
70+
71+
if (buttonElement) {
72+
buttonElement.style.left = `${mapRect.left + offset}px`
73+
if (finalTop !== undefined) {
74+
buttonElement.style.top = `${finalTop}px`
75+
buttonElement.style.bottom = ''
76+
} else if (finalBottom !== undefined) {
77+
buttonElement.style.bottom = `${finalBottom}px`
78+
buttonElement.style.top = ''
79+
}
80+
}
81+
}
82+
83+
updateButtonPosition()
84+
85+
let intersectionObserver: IntersectionObserver | null = null
86+
if (mapContainerRef.current) {
87+
intersectionObserver = new IntersectionObserver(
88+
() => {
89+
updateButtonPosition()
90+
},
91+
{
92+
threshold: 0,
93+
rootMargin: '0px',
94+
},
95+
)
96+
intersectionObserver.observe(mapContainerRef.current)
97+
}
98+
99+
window.document
100+
.getElementsByClassName('ant-layout-content')
101+
.item(0)
102+
?.addEventListener('scroll', updateButtonPosition)
103+
window.addEventListener('resize', updateButtonPosition)
104+
105+
return () => {
106+
if (intersectionObserver) {
107+
intersectionObserver.disconnect()
108+
}
109+
window.document
110+
.getElementsByClassName('ant-layout-content')
111+
.item(0)
112+
?.removeEventListener('scroll', updateButtonPosition)
113+
window.removeEventListener('resize', updateButtonPosition)
114+
}
115+
}, [mapContainerRef, buttonRef, isExpanded])
116+
}

src/pages/Map.scss

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@ main > div,
1717
position: relative;
1818
margin-bottom: 1em;
1919

20+
button {
21+
position: fixed;
22+
z-index: 2000;
23+
width: 2.6rem;
24+
height: 2.6rem;
25+
background: rgb(255 255 255 / 50%);
26+
border: 1px solid black;
27+
box-shadow:
28+
0 1px 3px rgb(0 0 0 / 8%),
29+
0 6px 16px rgb(0 0 0 / 10%);
30+
}
31+
32+
button:hover {
33+
background: rgb(255 255 255 / 65%);
34+
}
35+
36+
button:active {
37+
background: rgb(255 255 255 / 75%);
38+
}
39+
2040
.map-index {
2141
position: absolute;
2242
top: 1em;

src/pages/components/map-related/MapWithLocationsAndPath.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { OpenInFullRounded } from '@mui/icons-material'
22
import { IconButton } from '@mui/material'
3-
import { useCallback, useState } from 'react'
3+
import { useCallback, useRef, useState } from 'react'
44
import { MapContainer } from 'react-leaflet'
5+
import { useConstrainedFloatingButton } from 'src/hooks/useConstrainedFloatingButton'
56
import { Point } from 'src/pages/timeBasedMap'
67
import { MapProps } from './map-types'
78
import { MapContent } from './MapContent'
@@ -20,9 +21,18 @@ export function MapWithLocationsAndPath({
2021
const [isExpanded, setIsExpanded] = useState<boolean>(false)
2122
const toggleExpanded = useCallback(() => setIsExpanded((expanded) => !expanded), [])
2223

24+
const mapContainerRef = useRef<HTMLDivElement>(null)
25+
const buttonRef = useRef<HTMLButtonElement>(null)
26+
27+
useConstrainedFloatingButton(mapContainerRef, buttonRef, isExpanded)
28+
2329
return (
24-
<div className={`map-info ${isExpanded ? 'expanded' : 'collapsed'}`}>
25-
<IconButton color="primary" className="expand-button" onClick={toggleExpanded}>
30+
<div ref={mapContainerRef} className={`map-info ${isExpanded ? 'expanded' : 'collapsed'}`}>
31+
<IconButton
32+
ref={buttonRef}
33+
color="primary"
34+
className="expand-button"
35+
onClick={toggleExpanded}>
2636
<OpenInFullRounded fontSize="large" />
2737
</IconButton>
2838

src/pages/timeBasedMap/index.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import type { SiriVehicleLocationWithRelatedPydanticModel } from '@hasadna/open-bus-api-client'
22
import { OpenInFullRounded } from '@mui/icons-material'
33
import { Alert, CircularProgress, Grid, IconButton, Typography } from '@mui/material'
4-
import { useCallback, useEffect, useMemo, useState } from 'react'
4+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
55
import { useTranslation } from 'react-i18next'
66
import { MapContainer, Marker, Polyline, Popup, TileLayer, useMap } from 'react-leaflet'
77
import MarkerClusterGroup from 'react-leaflet-markercluster'
88
import useVehicleLocations from 'src/api/useVehicleLocations'
99
import dayjs from 'src/dayjs'
1010
import { useAgencyList } from 'src/hooks/useAgencyList'
11+
import { useConstrainedFloatingButton } from 'src/hooks/useConstrainedFloatingButton'
1112
import { BusToolTip } from 'src/pages/components/map-related/MapLayers/BusToolTip'
1213
import { INPUT_SIZE } from 'src/resources/sizes'
1314
import { DateSelector } from '../components/DateSelector'
@@ -41,6 +42,9 @@ export default function TimeBasedMapPage() {
4142
const [isExpanded, setIsExpanded] = useState<boolean>(false)
4243
const toggleExpanded = useCallback(() => setIsExpanded((expanded) => !expanded), [])
4344

45+
const mapContainerRef = useRef<HTMLDivElement>(null)
46+
const buttonRef = useRef<HTMLButtonElement>(null)
47+
4448
const position: Point = {
4549
loc: [32.3057988, 34.85478613], // arbitrary default value... Netanya - best city to live & die in
4650
color: 0,
@@ -86,6 +90,8 @@ export default function TimeBasedMapPage() {
8690
[locations],
8791
)
8892

93+
useConstrainedFloatingButton(mapContainerRef, buttonRef, isExpanded)
94+
8995
return (
9096
<PageContainer className="map-container">
9197
<Typography variant="h4" className="page-title">
@@ -152,8 +158,12 @@ export default function TimeBasedMapPage() {
152158
</Grid>
153159
<Grid size={{ xs: 1 }}>{isLoading && <CircularProgress size="20px" />}</Grid>
154160
</Grid>
155-
<div className={`map-info ${isExpanded ? 'expanded' : 'collapsed'}`}>
156-
<IconButton color="primary" className="expand-button" onClick={toggleExpanded}>
161+
<div ref={mapContainerRef} className={`map-info ${isExpanded ? 'expanded' : 'collapsed'}`}>
162+
<IconButton
163+
ref={buttonRef}
164+
color="primary"
165+
className="expand-button"
166+
onClick={toggleExpanded}>
157167
<OpenInFullRounded fontSize="large" />
158168
</IconButton>
159169
<MapContainer center={position.loc} zoom={8} scrollWheelZoom={true}>

src/pages/velocityHeatmap/index.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import { Stack } from '@mui/material'
2-
import React, { useContext, useState } from 'react'
1+
import { OpenInFullRounded } from '@mui/icons-material'
2+
import { IconButton, Stack } from '@mui/material'
3+
import React, { useCallback, useContext, useRef, useState } from 'react'
34
import { MapContainer, TileLayer } from 'react-leaflet'
45
import dayjs from 'src/dayjs'
6+
import { useConstrainedFloatingButton } from 'src/hooks/useConstrainedFloatingButton'
57
import { SearchContext } from '../../model/pageState'
68
import { DateNavigator } from '../components/dateNavigator/DateNavigator'
79
import { DateSelector } from '../components/DateSelector'
810
import { VelocityHeatmapLegend } from './components/VelocityHeatmapLegend'
911
import { VelocityHeatmapRectangles } from './components/VelocityHeatmapRectangles'
1012
import 'leaflet/dist/leaflet.css'
13+
import '../Map.scss'
1114

1215
const VIS_MODES = [
1316
{ key: 'avg', label: 'Visualize Avg Speed' },
@@ -18,16 +21,24 @@ const VIS_MODES = [
1821
const DEFAULT_ZOOM_LEVEL = 10
1922

2023
const VelocityHeatmapPage: React.FC = () => {
24+
const [isExpanded, setIsExpanded] = useState<boolean>(false)
25+
const toggleExpanded = useCallback(() => setIsExpanded((expanded) => !expanded), [])
26+
2127
const { search, setSearch } = useContext(SearchContext)
2228

2329
const [visMode, setVisMode] = useState<'avg' | 'std' | 'cv'>('avg')
2430
const [min, setMin] = useState(0)
2531
const [max, setMax] = useState(1)
2632

33+
const mapContainerRef = useRef<HTMLDivElement>(null)
34+
const buttonRef = useRef<HTMLButtonElement>(null)
35+
2736
const handleTimestampChange = (time: dayjs.Dayjs | null) => {
2837
setSearch((current) => ({ ...current, timestamp: time?.valueOf() ?? +new Date('2026-01-01') }))
2938
}
3039

40+
useConstrainedFloatingButton(mapContainerRef, buttonRef, isExpanded)
41+
3142
return (
3243
<div>
3344
<h1>Velocity Aggregation Heatmap</h1>
@@ -53,7 +64,15 @@ const VelocityHeatmapPage: React.FC = () => {
5364
</label>
5465
))}
5566
</div>
56-
<div style={{ height: '500px', width: '100%', margin: '16px 0' }}>
67+
68+
<div ref={mapContainerRef} className={`map-info ${isExpanded ? 'expanded' : 'collapsed'}`}>
69+
<IconButton
70+
ref={buttonRef}
71+
color="primary"
72+
className="expand-button"
73+
onClick={toggleExpanded}>
74+
<OpenInFullRounded fontSize="large" />
75+
</IconButton>
5776
<MapContainer
5877
center={[29.65, 34.6]}
5978
zoom={DEFAULT_ZOOM_LEVEL}

0 commit comments

Comments
 (0)