Skip to content

Commit

Permalink
Merge pull request #364 from dataforgoodfr/enhancement/frontend/vesse…
Browse files Browse the repository at this point in the history
…l-tooltip

Popup is ready
  • Loading branch information
HenriChabert authored Dec 13, 2024
2 parents e853d5a + 88177be commit 6523cc6
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 112 deletions.
2 changes: 0 additions & 2 deletions frontend/app/map/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { ZoneWithGeometry } from "@/types/zone"
import LeftPanel from "@/components/core/left-panel"
import MapControls from "@/components/core/map-controls"
import Map from "@/components/core/map/main-map"
import PositionPreview from "@/components/core/map/position-preview"
import { useMapStore } from "@/libs/stores/map-store"
import { useVesselsStore } from "@/libs/stores/vessels-store"
import { useLoaderStore } from "@/libs/stores/loader-store"
Expand Down Expand Up @@ -119,7 +118,6 @@ export default function MapPage() {
zones={zones}
/>
<MapControls zoneLoading={isLoadingZones} />
<PositionPreview />
</>
)
}
8 changes: 8 additions & 0 deletions frontend/components/core/left-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ export default function LeftPanel() {
mode: mapMode,
leftPanelOpened,
setLeftPanelOpened,
setActivePosition,
} = useMapStore(
useShallow((state) => ({
mode: state.mode,
leftPanelOpened: state.leftPanelOpened,
setLeftPanelOpened: state.setLeftPanelOpened,
setActivePosition: state.setActivePosition,
}))
)

Expand All @@ -72,6 +74,12 @@ export default function LeftPanel() {
svgControls.start(control)
}, [containerControls, leftPanelOpened, svgControls])

useEffect(() => {
if (leftPanelOpened) {
setActivePosition(null)
}
}, [leftPanelOpened])

const handleOpenClose = () => {
setLeftPanelOpened(!leftPanelOpened)
}
Expand Down
77 changes: 6 additions & 71 deletions frontend/components/core/map/deck-gl-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { GeoJsonLayer } from "@deck.gl/layers"
import DeckGL from "@deck.gl/react"
import { IconLayer, Layer, MapViewState, PolygonLayer } from "deck.gl"
import type { Feature, Geometry } from "geojson"
import { renderToString } from "react-dom/server"
import { Map as MapGL } from "react-map-gl/maplibre"
import { useShallow } from "zustand/react/shallow"

Expand All @@ -24,8 +23,8 @@ import { getVesselColorRGB } from "@/libs/colors"
import { useLoaderStore } from "@/libs/stores/loader-store"
import { useMapStore } from "@/libs/stores/map-store"
import { useTrackModeOptionsStore } from "@/libs/stores/track-mode-options-store"
import MapVesselTooltip from "@/components/ui/map-vessel-tooltip"
import MapZoneTooltip from "@/components/ui/map-zone-tooltip"

import { getPickObjectType } from "./utils"

type DeckGLMapProps = {
zones: ZoneWithGeometry[]
Expand Down Expand Up @@ -141,8 +140,9 @@ export default function DeckGLMap({
return trackedAndShownExcursions
}, [trackedAndShownVessels, excursions, excursionsIDsHidden])

const onMapClick = ({ layer }: PickingInfo) => {
if (layer?.id !== "vessels-latest-positions") {
const onMapClick = (info: PickingInfo) => {
const objectType = getPickObjectType(info)
if (objectType !== "vessel" && objectType !== "excursion") {
setActivePosition(null)
setFocusedExcursionID(null)
}
Expand Down Expand Up @@ -300,13 +300,10 @@ export default function DeckGLMap({
}

function onSegmentClick({ object }: PickingInfo) {
console.log("onSegmentClick", object)
const segment = object as Feature<Geometry, VesselExcursionSegmentGeo>
if (segment.properties.excursion_id !== focusedExcursionID) {
console.log("setFocusedExcursionID", segment.properties.excursion_id)
setFocusedExcursionID(segment.properties.excursion_id)
} else {
console.log("focusOnExcursion", segment.properties.excursion_id)
focusOnExcursion(segment.properties.excursion_id)
}
setLeftPanelOpened(true)
Expand Down Expand Up @@ -434,33 +431,6 @@ export default function DeckGLMap({
}
}, [focusedExcursionID])

// useEffect(() => {
// if (!mapTransitioning) {
// setFocusedExcursionID(null)
// }
// }, [viewState.latitude, viewState.longitude])

const getObjectType = (
object: VesselPosition | ZoneWithGeometry | GeoJsonLayer | undefined
) => {
if (!object) return null

// @ts-ignore
if (object?.properties?.excursion_id) {
return "excursion"
}

if ("vessel" in object) {
return "vessel"
}

if ("geometry" in object) {
return "zone"
}

return null
}

const isAMPDisplayed = displayedZones.includes(ZoneCategory.AMP)
const isTerritorialDisplayed = displayedZones.includes(
ZoneCategory.TERRITORIAL_SEAS
Expand Down Expand Up @@ -636,36 +606,6 @@ export default function DeckGLMap({
]
)

const getTooltip = ({
object,
}: Partial<
PickingInfo<VesselPosition | ZoneWithGeometry | GeoJsonLayer>
>) => {
const objectType = getObjectType(object)
const style = {
backgroundColor: "#fff",
fontSize: "0.8em",
borderRadius: "10px",
overflow: "hidden",
padding: "0px",
}
let element: React.ReactNode
if (objectType === "vessel") {
const vesselInfo = object as VesselPosition
element = <MapVesselTooltip vesselInfo={vesselInfo} />
} else if (objectType === "zone") {
const zoneInfo = object as ZoneWithGeometry
element = <MapZoneTooltip zoneInfo={zoneInfo} />
} else {
return null
}

return {
html: renderToString(element),
style,
}
}

return (
<DeckGL
viewState={viewState}
Expand All @@ -681,12 +621,6 @@ export default function DeckGLMap({
}}
onHover={onMapHover}
onClick={onMapClick}
getTooltip={({
object,
}: PickingInfo<VesselPosition | ZoneWithGeometry>) => {
if (!object) return null
return getTooltip({ object })
}}
>
<MapGL
mapStyle={`https://api.maptiler.com/maps/e9b57486-1b91-47e1-a763-6df391697483/style.json?key=${process.env.NEXT_PUBLIC_MAPTILER_TO}`}
Expand All @@ -695,3 +629,4 @@ export default function DeckGLMap({
</DeckGL>
)
}

118 changes: 106 additions & 12 deletions frontend/components/core/map/main-map.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
"use client"

import { useCallback, useEffect, useState } from "react"
import { useCallback, useEffect, useMemo, useState } from "react"
import type { PickingInfo } from "@deck.gl/core"
import { useMapStore } from "@/libs/stores/map-store"
import { useTrackModeOptionsStore } from "@/libs/stores"

import { VesselPositions } from "@/types/vessel"
import { VesselPosition, VesselPositions } from "@/types/vessel"
import { ZoneWithGeometry } from "@/types/zone"

import DeckGLMap from "./deck-gl-map"
import React from "react"
import { useShallow } from "zustand/react/shallow"
import MapVesselTooltip from "@/components/ui/map-vessel-tooltip"
import MapZoneTooltip from "@/components/ui/map-zone-tooltip"
import { getPickObjectType } from "./utils"

type MainMapProps = {
zones: ZoneWithGeometry[]
Expand All @@ -24,25 +30,113 @@ function CoordonatesIndicator({ coordinates }: { coordinates: string }) {
const MemoizedDeckGLMap = React.memo(DeckGLMap);

export default function MainMap({ zones }: MainMapProps) {
const [coordinates, setCoordinates] = useState<string>("-°N; -°E")

const onMapHover = useCallback(({ coordinate }: PickingInfo) => {
coordinate &&
setCoordinates(
coordinate[1].toFixed(3).toString() +
"°N; " +
coordinate[0].toFixed(3) +
"°E"
)
const { activePosition, setActivePosition } = useMapStore(
useShallow((state) => ({
viewState: state.viewState,
activePosition: state.activePosition,
setActivePosition: state.setActivePosition,
}))
)

const {
addTrackedVessel,
trackedVesselIDs,
removeTrackedVessel,
} = useTrackModeOptionsStore(useShallow((state) => ({
addTrackedVessel: state.addTrackedVessel,
trackedVesselIDs: state.trackedVesselIDs,
removeTrackedVessel: state.removeTrackedVessel,
})))

const [tooltipPosition, setTooltipPosition] = useState<{
top: number
left: number
} | null>(null)

const [hoverInfo, setHoverInfo] = useState<PickingInfo | null>(null)

const isVesselTracked = (vesselId: number) => {
return trackedVesselIDs.includes(vesselId)
}

const coordinates = useMemo(() => {
if (!hoverInfo) return "-°N; -°E"
const coordinate = hoverInfo.coordinate
if (!coordinate) return "-°N; -°E"
const latitude = coordinate[1].toFixed(3)
const longitude = coordinate[0].toFixed(3)
return `${latitude}°N; ${longitude}°E`
}, [hoverInfo])

useEffect(() => {
if (activePosition && hoverInfo) {
const top = hoverInfo.y > -1 ? hoverInfo.y : screen.height / 2 - 110
const left = hoverInfo.x > -1 ? hoverInfo.x : screen.width / 2 + 10

setTooltipPosition({
top,
left,
})
}
}, [activePosition])

const onMapHover = useCallback((hoverInfo: PickingInfo) => {
setHoverInfo(hoverInfo)
}, [])

const onToggleTrackedVessel = (vesselId: number) => {
if (trackedVesselIDs.includes(vesselId)) {
removeTrackedVessel(vesselId)
} else {
addTrackedVessel(vesselId)
}
}

const hoverTooltip = useMemo(() => {
if (!hoverInfo) return;

const { object, x, y } = hoverInfo;
const objectType = getPickObjectType(hoverInfo)

let element: React.ReactNode = null;

if (objectType === "vessel") {
const vesselInfo = object as VesselPosition
const vesselId = vesselInfo.vessel.id
if (activePosition?.vessel.id !== vesselId) {
element = <MapVesselTooltip vesselInfo={vesselInfo} top={y} left={x}/>
}
} else if (objectType === "zone") {
const zoneInfo = object as ZoneWithGeometry
element = <MapZoneTooltip zoneInfo={zoneInfo} top={y} left={x}/>
}

return element;
}, [hoverInfo, activePosition]);

return (
<div className="relative size-full">
<MemoizedDeckGLMap
zones={zones}
onHover={onMapHover}
/>
<CoordonatesIndicator coordinates={coordinates} />
{tooltipPosition && activePosition && (
<MapVesselTooltip
top={tooltipPosition.top}
left={tooltipPosition.left}
vesselInfo={activePosition}
isFrozen={true}
isSelected={isVesselTracked(activePosition.vessel.id)}
onClose={() => {
setActivePosition(null)
}}
onSelect={() => {
onToggleTrackedVessel(activePosition.vessel.id)
}}
/>
)}
{hoverTooltip}
</div>
)
}
21 changes: 21 additions & 0 deletions frontend/components/core/map/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { PickingInfo } from "@deck.gl/core"

export const getPickObjectType = (info: PickingInfo): "vessel" | "excursion" | "zone" | null => {
const { object } = info
if (!object) return null

// @ts-ignore
if (object?.properties?.excursion_id) {
return "excursion"
}

if ("vessel" in object) {
return "vessel"
}

if ("geometry" in object) {
return "zone"
}

return null
}
Loading

0 comments on commit 6523cc6

Please sign in to comment.