Skip to content

Commit 1d6e95d

Browse files
authored
Merge pull request #501 from open-pv/499-show-slope-of-object-on-hover
Show slope of object on hover
2 parents fd99e09 + 7a13d67 commit 1d6e95d

File tree

7 files changed

+158
-115
lines changed

7 files changed

+158
-115
lines changed

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/locales/de/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,5 +157,6 @@
157157
"balkonsolar": "Infos zu Balkonsolaranlagen",
158158
"companies": "Installateure in Ihrer Nähe",
159159
"bbe": "Kein eigenes Dach? Tritt doch einer Bürgerenergiegenossenschaft bei!"
160-
}
160+
},
161+
"slope": "Neigung"
161162
}

public/locales/en/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,6 @@
136136
"balkonsolar": "Information on balcony solar systems",
137137
"companies": "Installers near you",
138138
"bbe": "No own roof? Join a community energy cooperative!"
139-
}
139+
},
140+
"slope": "Slope"
140141
}

src/components/ThreeViewer/Controls/CustomMapControl.jsx

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,29 @@ function CustomMapControl() {
1111
const mouse = useRef(new THREE.Vector2())
1212
const { gl, camera, scene } = useThree()
1313

14-
let lastTap = 0
15-
16-
const handleInteraction = (event) => {
17-
event.preventDefault()
18-
19-
/**
20-
* Returns the list of intersected objects. An intersected object is an object
21-
* that lies directly below the mouse cursor.
22-
*/
23-
const getIntersects = () => {
24-
const isTouch = window.isTouchDevice
25-
const clientX = isTouch ? event.touches[0].clientX : event.clientX
26-
const clientY = isTouch ? event.touches[0].clientY : event.clientY
27-
28-
const rect = event.target.getBoundingClientRect()
29-
mouse.current.x = ((clientX - rect.left) / rect.width) * 2 - 1
30-
mouse.current.y = (-(clientY - rect.top) / rect.height) * 2 + 1
31-
32-
raycaster.current.setFromCamera(mouse.current, camera)
33-
34-
return raycaster.current.intersectObjects(scene.children, true)
35-
}
36-
const intersects = getIntersects()
37-
38-
if (intersects.length === 0) {
39-
console.log('No children in the intersected mesh.')
40-
return
41-
}
14+
/**
15+
* Returns the list of intersected objects. An intersected object is an object
16+
* that lies directly below the mouse cursor.
17+
*/
18+
const getIntersects = () => {
19+
const isTouch = window.isTouchDevice
20+
const clientX = isTouch ? event.touches[0].clientX : event.clientX
21+
const clientY = isTouch ? event.touches[0].clientY : event.clientY
22+
23+
const rect = event.target.getBoundingClientRect()
24+
mouse.current.x = ((clientX - rect.left) / rect.width) * 2 - 1
25+
mouse.current.y = (-(clientY - rect.top) / rect.height) * 2 + 1
26+
27+
raycaster.current.setFromCamera(mouse.current, camera)
28+
29+
return raycaster.current.intersectObjects(scene.children, true)
30+
}
4231

43-
// Filter out Sprites (ie the labels of PV systems)
32+
/**
33+
* Filter out Sprites (ie the labels of PV systems).
34+
* Returns the first element of the intersects list that is not a sprite.
35+
*/
36+
const ignoreSprites = (intersects) => {
4437
let i = 0
4538
while (i < intersects.length && intersects[i].object.type === 'Sprite') {
4639
i++
@@ -49,9 +42,20 @@ function CustomMapControl() {
4942
console.log('Only Sprite objects found in intersections.')
5043
return
5144
}
45+
return intersects[i]
46+
}
47+
48+
const handleDoubleClick = (event) => {
49+
event.preventDefault()
50+
51+
const intersects = getIntersects()
52+
53+
if (intersects.length === 0) {
54+
console.log('No children in the intersected mesh.')
55+
return
56+
}
5257

53-
let intersectedMesh = intersects[i].object
54-
console.log('Intersected Mesh', intersectedMesh)
58+
const intersectedMesh = ignoreSprites(intersects).object
5559

5660
if (!intersectedMesh) return
5761
if (!intersectedMesh.geometry.name) {
@@ -60,6 +64,7 @@ function CustomMapControl() {
6064
)
6165
return
6266
}
67+
console.log(intersectedMesh)
6368
if (
6469
intersectedMesh.geometry.name.includes('surrounding') ||
6570
intersectedMesh.geometry.name.includes('background')
@@ -71,17 +76,23 @@ function CustomMapControl() {
7176
}
7277
}
7378

74-
const handleDoubleClick = (event) => {
75-
handleInteraction(event)
79+
const handleMouseMove = (event) => {
80+
event.preventDefault()
81+
const intersects = getIntersects()
82+
const intersectedFace = ignoreSprites(intersects).face
83+
const slope = calculateSlopeFromNormal(intersectedFace.normal)
84+
sceneContext.setSlope(Math.round(slope))
7685
}
7786

7887
useEffect(() => {
7988
const canvas = gl.domElement
8089

8190
canvas.addEventListener('dblclick', handleDoubleClick)
91+
canvas.addEventListener('mousemove', handleMouseMove)
8292

8393
return () => {
8494
canvas.removeEventListener('dblclick', handleDoubleClick)
95+
canvas.addEventListener('mousemove', handleMouseMove)
8596
}
8697
}, [camera, scene])
8798

@@ -113,3 +124,9 @@ function CustomMapControl() {
113124
}
114125

115126
export default CustomMapControl
127+
128+
const calculateSlopeFromNormal = (normal) => {
129+
const up = new THREE.Vector3(0, 0, 1)
130+
const angleRad = normal.angleTo(up)
131+
return THREE.MathUtils.radToDeg(angleRad)
132+
}

src/components/ThreeViewer/Overlay.jsx

Lines changed: 90 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -270,84 +270,101 @@ function Overlay({ frontendState, setFrontendState, geoLocation }) {
270270
)
271271
}
272272

273+
const MouseHoverInfo = () => {
274+
return (
275+
<div className='attribution' id='footer-on-hover'>
276+
{t('slope') + ': ' + sceneContext.slope}°
277+
</div>
278+
)
279+
}
280+
273281
return (
274-
<OverlayWrapper>
275-
{sceneContext.selectedMesh.length > 0 && (
276-
<NotificationForSelectedBuilding geoLocation={geoLocation} />
277-
)}
278-
{sceneContext.selectedPVSystem.length > 0 && (
279-
<NotificationForSelectedPV />
280-
)}
282+
<>
283+
<MouseHoverInfo />
284+
<OverlayWrapper>
285+
{sceneContext.selectedMesh.length > 0 && (
286+
<NotificationForSelectedBuilding geoLocation={geoLocation} />
287+
)}
288+
{sceneContext.selectedPVSystem.length > 0 && (
289+
<NotificationForSelectedPV />
290+
)}
281291

282-
{frontendState == 'Results' && (
283-
<>
284-
<Button
285-
variant='subtle'
286-
onClick={() => {
287-
setFrontendState('DrawPV')
288-
}}
289-
>
290-
{t('button.drawPVSystem')}
291-
</Button>
292-
</>
293-
)}
294-
{frontendState == 'DrawPV' && (
295-
<>
296-
<Button variant='subtle' onClick={() => setFrontendState('Results')}>
297-
{t('button.cancel')}
298-
</Button>
299-
{sceneContext.pvPoints.length > 2 && (
300-
<Button variant='solid' onClick={handleCreatePVButtonClick}>
301-
{t('button.createPVSystem')}
292+
{frontendState == 'Results' && (
293+
<>
294+
<Button
295+
variant='subtle'
296+
onClick={() => {
297+
setFrontendState('DrawPV')
298+
}}
299+
>
300+
{t('button.drawPVSystem')}
302301
</Button>
303-
)}
304-
{sceneContext.pvPoints.length > 0 && (
305-
<>
306-
<Button
307-
variant='subtle'
308-
onClick={() => {
309-
sceneContext.setPVPoints(pvPoints.slice(0, -1))
310-
}}
311-
>
312-
{t('button.deleteLastPoint')}
302+
</>
303+
)}
304+
{frontendState == 'DrawPV' && (
305+
<>
306+
<Button
307+
variant='subtle'
308+
onClick={() => setFrontendState('Results')}
309+
>
310+
{t('button.cancel')}
311+
</Button>
312+
{sceneContext.pvPoints.length > 2 && (
313+
<Button variant='solid' onClick={handleCreatePVButtonClick}>
314+
{t('button.createPVSystem')}
313315
</Button>
314-
</>
315-
)}
316-
</>
317-
)}
316+
)}
317+
{sceneContext.pvPoints.length > 0 && (
318+
<>
319+
<Button
320+
variant='subtle'
321+
onClick={() => {
322+
sceneContext.setPVPoints(pvPoints.slice(0, -1))
323+
}}
324+
>
325+
{t('button.deleteLastPoint')}
326+
</Button>
327+
</>
328+
)}
329+
</>
330+
)}
318331

319-
<Menu.Root>
320-
<Menu.Trigger>
321-
<Button variant='subtle' size='sm'>
322-
{t('button.more')}
323-
</Button>
324-
</Menu.Trigger>
325-
<Menu.Content>
326-
<Menu.Item
327-
value='advertisment'
328-
onClick={() => setIsOpenAdvertisment(true)}
329-
>
330-
{t('adbox.button')}
331-
</Menu.Item>
332-
<Menu.Item
333-
value='options'
334-
onClick={() => setIsOpenOptionsDialog(true)}
335-
>
336-
{t('button.options')}
337-
</Menu.Item>
338-
<Menu.Item value='help' onClick={() => setIsOpenControlHelp(true)}>
339-
{t('mapControlHelp.button')}
340-
</Menu.Item>
341-
<Menu.Item value='legend' onClick={() => setIsOpenColorLegend(true)}>
342-
{t(`colorLegend.button`)}
343-
</Menu.Item>
344-
</Menu.Content>
345-
</Menu.Root>
346-
<AdvertismentDialog />
347-
<OptionsDialog />
348-
<ControlHelperDialog />
349-
<ColorLegend />
350-
</OverlayWrapper>
332+
<Menu.Root>
333+
<Menu.Trigger>
334+
<Button variant='subtle' size='sm'>
335+
{t('button.more')}
336+
</Button>
337+
</Menu.Trigger>
338+
<Menu.Content>
339+
<Menu.Item
340+
value='advertisment'
341+
onClick={() => setIsOpenAdvertisment(true)}
342+
>
343+
{t('adbox.button')}
344+
</Menu.Item>
345+
<Menu.Item
346+
value='options'
347+
onClick={() => setIsOpenOptionsDialog(true)}
348+
>
349+
{t('button.options')}
350+
</Menu.Item>
351+
<Menu.Item value='help' onClick={() => setIsOpenControlHelp(true)}>
352+
{t('mapControlHelp.button')}
353+
</Menu.Item>
354+
<Menu.Item
355+
value='legend'
356+
onClick={() => setIsOpenColorLegend(true)}
357+
>
358+
{t(`colorLegend.button`)}
359+
</Menu.Item>
360+
</Menu.Content>
361+
</Menu.Root>
362+
<AdvertismentDialog />
363+
<OptionsDialog />
364+
<ControlHelperDialog />
365+
<ColorLegend />
366+
</OverlayWrapper>
367+
</>
351368
)
352369
}
353370

src/components/ThreeViewer/Scene.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const Scene = ({
3434
const [selectedMesh, setSelectedMesh] = useState([])
3535
// highlighted PVSystems for deletion or calculation
3636
const [selectedPVSystem, setSelectedPVSystem] = useState([])
37+
const [slope, setSlope] = useState('')
3738

3839
window.setPVPoints = setPVPoints
3940
const position = [
@@ -58,6 +59,8 @@ const Scene = ({
5859
setSelectedMesh,
5960
showTerrain,
6061
setShowTerrain,
62+
slope,
63+
setSlope,
6164
}}
6265
>
6366
<Overlay

src/static/css/main.css

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@
2121
font-weight: 400;
2222
letter-spacing: 0.25em;
2323
text-transform: uppercase;
24-
}
25-
26-
.attribution {
2724
padding: 10px;
2825
background-color: #ffffffa0;
2926
width: fit-content;
@@ -63,3 +60,9 @@ button.maplibregl-popup-close-button {
6360
.maplibregl-popup-content {
6461
font-size: 1.5em;
6562
}
63+
64+
#footer-on-hover {
65+
right: 0;
66+
left: auto;
67+
z-index: 9999;
68+
}

0 commit comments

Comments
 (0)