From a1c957600dac4b69a5869d8903d7cde7b5141bbf Mon Sep 17 00:00:00 2001 From: Henri Chabert Date: Fri, 29 Nov 2024 14:35:34 +0100 Subject: [PATCH] Changed vessel icons, and handle click on map --- frontend/components/core/map/main-map.tsx | 163 ++++++++++------------ frontend/public/img/map-vessel.png | Bin 0 -> 1146 bytes 2 files changed, 71 insertions(+), 92 deletions(-) create mode 100644 frontend/public/img/map-vessel.png diff --git a/frontend/components/core/map/main-map.tsx b/frontend/components/core/map/main-map.tsx index cd79c5c2..b52c9fb5 100644 --- a/frontend/components/core/map/main-map.tsx +++ b/frontend/components/core/map/main-map.tsx @@ -2,17 +2,18 @@ import "maplibre-gl/dist/maplibre-gl.css" -import { useEffect } from "react" import type { PickingInfo } from "@deck.gl/core" import { GeoJsonLayer } from "@deck.gl/layers" -import { SimpleMeshLayer } from "@deck.gl/mesh-layers" import DeckGL from "@deck.gl/react" -import { OBJLoader } from "@loaders.gl/obj" import chroma from "chroma-js" -import { Layer, MapViewState, PolygonLayer, ScatterplotLayer } from "deck.gl" +import { IconLayer, Layer, MapViewState, PolygonLayer } from "deck.gl" +import { useEffect } from "react" import { renderToString } from "react-dom/server" import { Map as MapGL } from "react-map-gl/maplibre" +import { useMapStore } from "@/components/providers/map-store-provider" +import MapTooltip from "@/components/ui/tooltip-map-template" +import ZoneMapTooltip from "@/components/ui/zone-map-tooltip" import { VesselExcursionSegment, VesselExcursionSegmentGeo, @@ -22,11 +23,6 @@ import { VesselPositions, } from "@/types/vessel" import { ZoneWithGeometry } from "@/types/zone" -import MapTooltip from "@/components/ui/tooltip-map-template" -import ZoneMapTooltip from "@/components/ui/zone-map-tooltip" -import { useMapStore } from "@/components/providers/map-store-provider" - -const MESH_URL_LOCAL = `../../../data/mesh/boat.obj` type CoreMapProps = { vesselsPositions: VesselPositions @@ -56,12 +52,20 @@ export default function CoreMap({ // Use a piece of state that changes when `activePosition` changes to force re-render // const [layerKey, setLayerKey] = useState(0) + const VESSEL_COLOR = [16, 181, 16, 210]; + const TRACKED_VESSEL_COLOR = [128, 16, 189, 210]; + function getColorFromValue(value: number): [number, number, number] { const scale = chroma.scale(["yellow", "red", "black"]).domain([0, 15]) const color = scale(value).rgb() return [Math.round(color[0]), Math.round(color[1]), Math.round(color[2])] } + const isVesselSelected = (vp: VesselPosition) => { + return vp.vessel.id === activePosition?.vessel.id || + trackedVesselIDs.includes(vp.vessel.id) + } + // useEffect(() => { // // This will change the key of the layer, forcing it to re-render when `activePosition` changes // setLayerKey((prevKey) => prevKey + 1) @@ -71,27 +75,30 @@ export default function CoreMap({ setLatestPositions(vesselsPositions) }, [setLatestPositions, vesselsPositions]) - const latestPositions = new ScatterplotLayer({ + const latestPositions = new IconLayer({ id: `vessels-latest-positions`, data: vesselsPositions, getPosition: (vp: VesselPosition) => [ vp?.position?.coordinates[0], vp?.position?.coordinates[1], ], - stroked: false, - radiusUnits: "meters", - getRadius: (vp: VesselPosition) => vp.vessel.length, - radiusMinPixels: 3, - radiusMaxPixels: 25, - radiusScale: 200, - getFillColor: (vp: VesselPosition) => { - return vp.vessel.id === activePosition?.vessel.id || - trackedVesselIDs.includes(vp.vessel.id) - ? [128, 16, 189, 210] - : [16, 181, 16, 210] + getAngle: (vp: VesselPosition) => vp.heading ? Math.round(vp.heading) : 0, + getIcon: () => "default", + iconAtlas: "../../../img/map-vessel.png", + iconMapping: { + default: { + x: 0, + y: 0, + width: 35, + height: 27, + mask: true, + }, }, - getLineColor: [0, 0, 0], - getLineWidth: 3, + getSize: 16, + getColor: (vp: VesselPosition) => { + return new Uint8ClampedArray(isVesselSelected(vp) ? TRACKED_VESSEL_COLOR : VESSEL_COLOR) + }, + pickable: true, onClick: ({ object }) => { setActivePosition(object as VesselPosition) @@ -106,7 +113,7 @@ export default function CoreMap({ // }) }, updateTriggers: { - getFillColor: [activePosition?.vessel.id, trackedVesselIDs], + getColor: [activePosition?.vessel.id, trackedVesselIDs], }, }) @@ -131,49 +138,10 @@ export default function CoreMap({ }) }) - const positions_mesh_layer = new SimpleMeshLayer({ - id: `vessels-positions-mesh-layer`, - data: vesselsPositions, - mesh: MESH_URL_LOCAL, - getPosition: (vp: VesselPosition) => [ - vp?.position?.coordinates[0], - vp?.position?.coordinates[1], - ], - getColor: (vp: VesselPosition) => { - return vp.vessel.id === activePosition?.vessel.id || - trackedVesselIDs.includes(vp.vessel.id) - ? [128, 16, 189, 210] - : [16, 181, 16, 210] - }, - getOrientation: (vp: VesselPosition) => [ - 0, - Math.round(vp.heading ? vp.heading : 0), - 90, - ], - getScale: (vp: VesselPosition) => [ - vp.vessel.length, - vp.vessel.length * 1.5, - vp.vessel.length / 1.5, - ], - scaleUnits: "pixels", - sizeScale: 100, - pickable: false, - onClick: ({ object }) => { - setActivePosition(object as VesselPosition) - // setViewState({ - // ...viewState, - // longitude: object?.position?.coordinates[0], - // latitude: object?.position?.coordinates[1], - // zoom: 7, - // transitionInterpolator: new FlyToInterpolator({ speed: 2 }), - // transitionDuration: "auto", - // }) - }, - updateTriggers: { - getFillColor: [activePosition?.vessel.id, trackedVesselIDs], - }, - loaders: [OBJLoader], - }) + const getObjectType = (object: VesselPosition | ZoneWithGeometry | undefined) => { + if (!object) return null + return "vessel" in object ? "vessel" : "zone" + } const zoneLayer = new PolygonLayer({ id: `zones-layer`, @@ -218,43 +186,54 @@ export default function CoreMap({ !isLoading.zones && zoneLayer, !isLoading.vessels && !isLoading.positions && tracksByVesselAndVoyage, !isLoading.positions && latestPositions, - !isLoading.vessels && !isLoading.positions && positions_mesh_layer, ].filter(Boolean) as Layer[] + const onMapClick = ({ picked, object }: PickingInfo) => { + if (picked) { + setActivePosition(object as VesselPosition) + } else { + setActivePosition(null) + } + } + + const getTooltip = ({ object }: Partial>) => { + 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 = + } else if (objectType === "zone") { + const zoneInfo = object as ZoneWithGeometry + element = + } + return { + html: renderToString(element), + style, + } + } + return ( setViewState(e.viewState as MapViewState)} + getCursor={({ isHovering, isDragging }) => { + return isDragging ? "move" : isHovering ? "pointer" : "grab"; + }} + onClick={onMapClick} getTooltip={({ object, }: PickingInfo) => { if (!object) return null - - if ("vessel" in object) { - return { - html: renderToString(), - style: { - backgroundColor: "#fff", - fontSize: "0.8em", - borderRadius: "10px", - overflow: "hidden", - padding: "0px", - }, - } - } else { - return { - html: renderToString(), - style: { - backgroundColor: "#fff", - fontSize: "0.8em", - borderRadius: "10px", - overflow: "hidden", - padding: "0px", - }, - } - } + return getTooltip({ object }) }} > K~#7F&6mqd zBv%y1Z&km$eI%kt8gV3J1YH+z^EX37Iuo9@;LEtvLErbR^N){-qyH$7|#9gViwY5aF zR%l6ee=CCx)rp4(nQ59|7mLN@@$s>XGKx40`xEzAqYQqE$~V-*t(HtCzkpEPbzOaS zcBc3C_JV#Vm(;`EaRZR}ESJkojgF4KIWsepRecIlQV$EvUidDZPQRPYW*?1=jEvFF zMR60F*@HwSst?3aM(Ic-$I? zwneP8=xY+d?dj=hyIQUK^y_$%Dfe7}GRm?n10so`p`jEL)12a%lYPUuYQH3y^C{&g zZ2CJyLK2`2gh&f@Y{@SW(SdB-WoDdk_V@P<+qR9du`#33Xv9Hfaw;j`>X!s%mH2R2 z*9L9cP5K2w#5z4ab#RXl4;?+KR~BaAh){r=^ufV_$F)nk!h(~rK+??gP03%3~d)u{^H`I*Nen6WV#0h zoi8MhRBfD}pNrhG$X6`>BmEY8n?gd|5fc#tZ0bPNA>KqCnI~7*pO}~kc6WDmFA~qV zsoR9h*4CD>y}g~P*XwDNDKwkSA%4n3A{n_w2!=UB;)5|^JP0uong=vF%iGx42<}p& z+ipAu@kAEaBqjwB%{7;pxVjynfeM$XbqSFMzqL@H#g(DN6E`(bsev0> zv$M0-{QSJlFmH8rRopf(C9ab(